diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index a985680567..ff9f70a649 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -29,7 +29,6 @@ use auction::AuctionCmd; use liquidity_position::PositionCmd; use penumbra_asset::{asset, asset::Metadata, Value, STAKING_TOKEN_ASSET_ID}; use penumbra_dex::{lp::position, swap_claim::SwapClaimPlan}; -use penumbra_fee::Fee; use penumbra_governance::{proposal::ProposalToml, proposal_state::State as ProposalState, Vote}; use penumbra_keys::{keys::AddressIndex, Address}; use penumbra_num::Amount; @@ -321,6 +320,9 @@ impl TxCmd { } pub async fn exec(&self, app: &mut App) -> Result<()> { + // TODO: use a command line flag to determine the fee token, + // and pull the appropriate GasPrices out of this rpc response, + // the rest should follow let gas_prices = app .view .as_mut() @@ -423,6 +425,7 @@ impl TxCmd { } => { let input = input.parse::()?; let into = asset::REGISTRY.parse_unit(into.as_str()).base(); + let fee_tier: FeeTier = (*fee_tier).into(); let fvk = app.config.full_viewing_key.clone(); @@ -434,20 +437,14 @@ impl TxCmd { let mut planner = Planner::new(OsRng); planner .set_gas_prices(gas_prices.clone()) - .set_fee_tier((*fee_tier).into()); - // The swap claim requires a pre-paid fee, however gas costs might change in the meantime. - // This shouldn't be an issue, since the planner will account for the difference and add additional - // spends alongside the swap claim transaction as necessary. - // - // Regardless, we apply a gas adjustment factor of 2.0 up-front to reduce the likelihood of - // requiring an additional spend at the time of claim. - // - // Since the swap claim fee needs to be passed in to the planner to build the swap (it is - // part of the `SwapPlaintext`), we can't use the planner to estimate the fee and need to - // call the helper method directly. - let estimated_claim_fee = Fee::from_staking_token_amount( - Amount::from(2u32) * gas_prices.fee(&swap_claim_gas_cost()), - ); + .set_fee_tier(fee_tier.into()); + + // We don't expect much of a drift in gas prices in a few blocks, and the fee tier + // adjustments should be enough to cover it. + let estimated_claim_fee = gas_prices + .fee(&swap_claim_gas_cost()) + .apply_tier(fee_tier.into()); + planner.swap(input, into.id(), estimated_claim_fee, claim_address)?; let plan = planner @@ -503,7 +500,7 @@ impl TxCmd { let mut planner = Planner::new(OsRng); planner .set_gas_prices(gas_prices) - .set_fee_tier((*fee_tier).into()); + .set_fee_tier(fee_tier.into()); let plan = planner .swap_claim(SwapClaimPlan { swap_plaintext, diff --git a/crates/bin/pcli/src/network.rs b/crates/bin/pcli/src/network.rs index d9f0502f67..c6aa230cb5 100644 --- a/crates/bin/pcli/src/network.rs +++ b/crates/bin/pcli/src/network.rs @@ -1,17 +1,15 @@ use anyhow::Context; use decaf377_rdsa::{Signature, SpendAuth}; use futures::{FutureExt, TryStreamExt}; -use penumbra_fee::GasPrices; use penumbra_governance::ValidatorVoteBody; use penumbra_proto::{ custody::v1::{AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest}, util::tendermint_proxy::v1::tendermint_proxy_service_client::TendermintProxyServiceClient, view::v1::broadcast_transaction_response::Status as BroadcastStatus, - view::v1::GasPricesRequest, DomainType, }; use penumbra_stake::validator::Validator; -use penumbra_transaction::{gas::GasCost, txhash::TransactionId, Transaction, TransactionPlan}; +use penumbra_transaction::{txhash::TransactionId, Transaction, TransactionPlan}; use penumbra_view::ViewClient; use std::future::Future; use tonic::transport::{Channel, ClientTlsConfig}; @@ -24,25 +22,7 @@ impl App { &mut self, plan: TransactionPlan, ) -> anyhow::Result { - let gas_prices: GasPrices = self - .view - .as_mut() - .context("view service must be initialized")? - .gas_prices(GasPricesRequest {}) - .await? - .into_inner() - .gas_prices - .expect("gas prices must be available") - .try_into()?; let transaction = self.build_transaction(plan).await?; - let gas_cost = transaction.gas_cost(); - let fee = gas_prices.fee(&gas_cost); - assert!( - transaction.transaction_parameters().fee.amount() >= fee, - "paid fee {} must be greater than minimum fee {}", - transaction.transaction_parameters().fee.amount(), - fee - ); self.submit_transaction(transaction).await } diff --git a/crates/bin/pd/src/testnet/generate.rs b/crates/bin/pd/src/testnet/generate.rs index cc1d1a46e6..a03f33647a 100644 --- a/crates/bin/pd/src/testnet/generate.rs +++ b/crates/bin/pd/src/testnet/generate.rs @@ -4,6 +4,7 @@ use crate::testnet::config::{get_testnet_dir, TestnetTendermintConfig, ValidatorKeys}; use anyhow::{Context, Result}; use penumbra_app::params::AppParameters; +use penumbra_asset::{asset, STAKING_TOKEN_ASSET_ID}; use penumbra_fee::genesis::Content as FeeContent; use penumbra_governance::genesis::Content as GovernanceContent; use penumbra_keys::{keys::SpendKey, Address}; @@ -223,7 +224,24 @@ impl TestnetConfig { compact_block_space_price: gas_price_simple, verification_price: gas_price_simple, execution_price: gas_price_simple, + asset_id: *STAKING_TOKEN_ASSET_ID, }, + fixed_alt_gas_prices: vec![ + penumbra_fee::GasPrices { + block_space_price: 10 * gas_price_simple, + compact_block_space_price: 10 * gas_price_simple, + verification_price: 10 * gas_price_simple, + execution_price: 10 * gas_price_simple, + asset_id: asset::REGISTRY.parse_unit("gm").id(), + }, + penumbra_fee::GasPrices { + block_space_price: 10 * gas_price_simple, + compact_block_space_price: 10 * gas_price_simple, + verification_price: 10 * gas_price_simple, + execution_price: 10 * gas_price_simple, + asset_id: asset::REGISTRY.parse_unit("gn").id(), + }, + ], }, }, governance_content: GovernanceContent { diff --git a/crates/core/app/src/action_handler/transaction/stateful.rs b/crates/core/app/src/action_handler/transaction/stateful.rs index 7241d1a6a6..0161443d5f 100644 --- a/crates/core/app/src/action_handler/transaction/stateful.rs +++ b/crates/core/app/src/action_handler/transaction/stateful.rs @@ -83,28 +83,47 @@ pub async fn fee_greater_than_base_fee( state: S, transaction: &Transaction, ) -> Result<()> { - let current_gas_prices = state - .get_gas_prices() - .await - .expect("gas prices must be present in state"); - - let transaction_base_price = current_gas_prices.fee(&transaction.gas_cost()); + // Check whether the user is requesting to pay fees in the native token + // or in an alternative fee token. let user_supplied_fee = transaction.transaction_body().transaction_parameters.fee; - let user_supplied_fee_amount = user_supplied_fee.amount(); - let user_supplied_fee_asset_id = user_supplied_fee.asset_id(); + let current_gas_prices = + if user_supplied_fee.asset_id() == *penumbra_asset::STAKING_TOKEN_ASSET_ID { + state + .get_gas_prices() + .await + .expect("gas prices must be present in state") + } else { + let alt_gas_prices = state + .get_alt_gas_prices() + .await + .expect("alt gas prices must be present in state"); + alt_gas_prices + .into_iter() + .find(|prices| prices.asset_id == user_supplied_fee.asset_id()) + .ok_or_else(|| { + anyhow::anyhow!( + "fee token {} not recognized by the chain", + user_supplied_fee.asset_id() + ) + })? + }; + + // Double check that the gas price assets match. ensure!( - user_supplied_fee_amount >= transaction_base_price, - "fee must be greater than or equal to the transaction base price (supplied: {}, base: {})", - user_supplied_fee_amount, - transaction_base_price + current_gas_prices.asset_id == user_supplied_fee.asset_id(), + "unexpected mismatch between fee and queried gas prices (expected: {}, found: {})", + user_supplied_fee.asset_id(), + current_gas_prices.asset_id, ); - // We split the check to provide granular error messages. + let transaction_base_fee = current_gas_prices.fee(&transaction.gas_cost()); + ensure!( - user_supplied_fee_asset_id == *penumbra_asset::STAKING_TOKEN_ASSET_ID, - "fee must be paid in staking tokens (found: {})", - user_supplied_fee_asset_id + user_supplied_fee.amount() >= transaction_base_fee.amount(), + "fee must be greater than or equal to the transaction base price (supplied: {}, base: {})", + user_supplied_fee.amount(), + transaction_base_fee.amount(), ); Ok(()) diff --git a/crates/core/app/src/params/change.rs b/crates/core/app/src/params/change.rs index a4ba164ba8..e4a91053e1 100644 --- a/crates/core/app/src/params/change.rs +++ b/crates/core/app/src/params/change.rs @@ -69,9 +69,11 @@ impl AppParameters { DistributionsParameters { staking_issuance_per_block: _, }, - fee_params: FeeParameters { - fixed_gas_prices: _, - }, + fee_params: + FeeParameters { + fixed_gas_prices: _, + fixed_alt_gas_prices: _, + }, funding_params: FundingParameters {}, governance_params: GovernanceParameters { @@ -165,9 +167,11 @@ impl AppParameters { DistributionsParameters { staking_issuance_per_block: _, }, - fee_params: FeeParameters { - fixed_gas_prices: _, - }, + fee_params: + FeeParameters { + fixed_gas_prices: _, + fixed_alt_gas_prices: _, + }, funding_params: FundingParameters {}, governance_params: GovernanceParameters { diff --git a/crates/core/asset/src/asset/registry.rs b/crates/core/asset/src/asset/registry.rs index 72c2eb02d0..3ab448e185 100644 --- a/crates/core/asset/src/asset/registry.rs +++ b/crates/core/asset/src/asset/registry.rs @@ -416,14 +416,15 @@ pub static REGISTRY: Lazy = Lazy::new(|| { denom: format!("mvoted_on_{data}"), }, ]) - }) as for<'r> fn(&'r str) -> _) - .add_asset( - "^auctionnft_(?P[a-z_0-9]+_pauctid1[a-zA-HJ-NP-Z0-9]+)$", - &[ /* no display units - nft, unit 1 */ ], - (|data: &str| { - assert!(!data.is_empty()); - denom_metadata::Inner::new(format!("auctionnft_{data}"), vec![]) - }) as for<'r> fn(&'r str) -> _, - ) + }) as for<'r> fn(&'r str) -> _ + ) + .add_asset( + "^auctionnft_(?P[a-z_0-9]+_pauctid1[a-zA-HJ-NP-Z0-9]+)$", + &[ /* no display units - nft, unit 1 */ ], + (|data: &str| { + assert!(!data.is_empty()); + denom_metadata::Inner::new(format!("auctionnft_{data}"), vec![]) + }) as for<'r> fn(&'r str) -> _, + ) .build() }); diff --git a/crates/core/component/compact-block/src/compact_block.rs b/crates/core/component/compact-block/src/compact_block.rs index 2041a0995f..0e5a8d2d2b 100644 --- a/crates/core/component/compact-block/src/compact_block.rs +++ b/crates/core/component/compact-block/src/compact_block.rs @@ -36,8 +36,10 @@ pub struct CompactBlock { pub swap_outputs: BTreeMap, /// Set if the app parameters have been updated. Notifies the client that it should re-sync from the fullnode RPC. pub app_parameters_updated: bool, - /// Updated gas prices, if they have changed. + /// Updated gas prices for the native token, if they have changed. pub gas_prices: Option, + /// Updated gas prices for alternative fee tokens, if they have changed. + pub alt_gas_prices: Vec, // The epoch index pub epoch_index: u64, // **IMPORTANT NOTE FOR FUTURE HUMANS**: if you want to add new fields to the `CompactBlock`, @@ -59,6 +61,7 @@ impl Default for CompactBlock { swap_outputs: BTreeMap::new(), app_parameters_updated: false, gas_prices: None, + alt_gas_prices: Vec::new(), epoch_index: 0, } } @@ -73,6 +76,7 @@ impl CompactBlock { || self.proposal_started // need to process proposal start || self.app_parameters_updated // need to save latest app parameters || self.gas_prices.is_some() // need to save latest gas prices + || !self.alt_gas_prices.is_empty() // need to save latest alt gas prices } } @@ -98,6 +102,7 @@ impl From for pb::CompactBlock { swap_outputs: cb.swap_outputs.into_values().map(Into::into).collect(), app_parameters_updated: cb.app_parameters_updated, gas_prices: cb.gas_prices.map(Into::into), + alt_gas_prices: cb.alt_gas_prices.into_iter().map(Into::into).collect(), epoch_index: cb.epoch_index, } } @@ -136,6 +141,11 @@ impl TryFrom for CompactBlock { proposal_started: value.proposal_started, app_parameters_updated: value.app_parameters_updated, gas_prices: value.gas_prices.map(TryInto::try_into).transpose()?, + alt_gas_prices: value + .alt_gas_prices + .into_iter() + .map(GasPrices::try_from) + .collect::>>()?, epoch_index: value.epoch_index, }) } diff --git a/crates/core/component/compact-block/src/component/manager.rs b/crates/core/component/compact-block/src/component/manager.rs index e3bb1ce4fc..8824a0c16a 100644 --- a/crates/core/component/compact-block/src/component/manager.rs +++ b/crates/core/component/compact-block/src/component/manager.rs @@ -52,14 +52,19 @@ trait Inner: StateWrite { // Check to see if the gas prices have changed, and include them in the compact block // if they have (this is signaled by `penumbra_fee::StateWriteExt::put_gas_prices`): - let gas_prices = if self.gas_prices_changed() || height == 0 { - Some( - self.get_gas_prices() + let (gas_prices, alt_gas_prices) = if self.gas_prices_changed() || height == 0 { + ( + Some( + self.get_gas_prices() + .await + .context("could not get gas prices")?, + ), + self.get_alt_gas_prices() .await - .context("could not get gas prices")?, + .context("could not get alt gas prices")?, ) } else { - None + (None, Vec::new()) }; let fmd_parameters = if height == 0 { @@ -133,6 +138,7 @@ trait Inner: StateWrite { fmd_parameters, app_parameters_updated, gas_prices, + alt_gas_prices, epoch_index, }; diff --git a/crates/core/component/fee/src/component.rs b/crates/core/component/fee/src/component.rs index cb45a9b915..b5dbb26071 100644 --- a/crates/core/component/fee/src/component.rs +++ b/crates/core/component/fee/src/component.rs @@ -23,8 +23,6 @@ impl Component for Fee { match app_state { Some(genesis) => { state.put_fee_params(genesis.fee_params.clone()); - // Put the initial gas prices - state.put_gas_prices(genesis.fee_params.fixed_gas_prices); } None => { /* perform upgrade specific check */ } } diff --git a/crates/core/component/fee/src/component/rpc.rs b/crates/core/component/fee/src/component/rpc.rs index 7716e20037..f93d1bac44 100644 --- a/crates/core/component/fee/src/component/rpc.rs +++ b/crates/core/component/fee/src/component/rpc.rs @@ -30,6 +30,7 @@ impl QueryService for Server { Ok(tonic::Response::new(pb::CurrentGasPricesResponse { gas_prices: Some(gas_prices.into()), + alt_gas_prices: Vec::new(), })) } } diff --git a/crates/core/component/fee/src/component/view.rs b/crates/core/component/fee/src/component/view.rs index dffaac147a..89d2aef8b2 100644 --- a/crates/core/component/fee/src/component/view.rs +++ b/crates/core/component/fee/src/component/view.rs @@ -7,10 +7,6 @@ use crate::{params::FeeParameters, state_key, GasPrices}; /// This trait provides read access to fee-related parts of the Penumbra /// state store. -/// -/// Note: the `get_` methods in this trait assume that the state store has been -/// initialized, so they will error on an empty state. -//#[async_trait(?Send)] #[async_trait] pub trait StateReadExt: StateRead { /// Gets the fee parameters from the JMT. @@ -20,11 +16,24 @@ pub trait StateReadExt: StateRead { .ok_or_else(|| anyhow!("Missing FeeParameters")) } - /// Gets the gas prices from the JMT. + /// Gets the current gas prices for the fee token. async fn get_gas_prices(&self) -> Result { - self.get(state_key::gas_prices()) - .await? - .ok_or_else(|| anyhow!("Missing GasPrices")) + // When we implement dynamic gas pricing, we will want + // to read the prices we computed. But until then, we need to + // read these from the _fee params_ instead, since those are + // the values that will get updated by governance. + let params = self.get_fee_params().await?; + Ok(params.fixed_gas_prices) + } + + /// Gets the current gas prices for alternative fee tokens. + async fn get_alt_gas_prices(&self) -> Result> { + // When we implement dynamic gas pricing, we will want + // to read the prices we computed. But until then, we need to + // read these from the _fee params_ instead, since those are + // the values that will get updated by governance. + let params = self.get_fee_params().await?; + Ok(params.fixed_alt_gas_prices) } /// Returns true if the gas prices have been changed in this block. @@ -38,17 +47,17 @@ impl StateReadExt for T {} /// This trait provides write access to common parts of the Penumbra /// state store. -/// -/// Note: the `get_` methods in this trait assume that the state store has been -/// initialized, so they will error on an empty state. -//#[async_trait(?Send)] #[async_trait] pub trait StateWriteExt: StateWrite { /// Writes the provided fee parameters to the JMT. fn put_fee_params(&mut self, params: FeeParameters) { - self.put(state_key::fee_params().into(), params) + self.put(state_key::fee_params().into(), params); + // This could have changed the gas prices, so mark them as changed. + self.object_put(state_key::gas_prices_changed(), ()); } + /* + We shouldn't be setting gas prices directly, until we have dynamic gas pricing. /// Writes the provided gas prices to the JMT. fn put_gas_prices(&mut self, gas_prices: GasPrices) { // Change the gas prices: @@ -57,6 +66,7 @@ pub trait StateWriteExt: StateWrite { // Mark that they've changed self.object_put(state_key::gas_prices_changed(), ()); } + */ } impl StateWriteExt for T {} diff --git a/crates/core/component/fee/src/gas.rs b/crates/core/component/fee/src/gas.rs index 8e16fe4cb5..c4a933fb88 100644 --- a/crates/core/component/fee/src/gas.rs +++ b/crates/core/component/fee/src/gas.rs @@ -3,11 +3,14 @@ use std::{ ops::{Add, AddAssign}, }; +use penumbra_asset::{asset, Value, STAKING_TOKEN_ASSET_ID}; use serde::{Deserialize, Serialize}; use penumbra_num::Amount; use penumbra_proto::{core::component::fee::v1 as pb, DomainType}; +use crate::Fee; + /// Represents the different resources that a transaction can consume, /// for purposes of calculating multidimensional fees based on real /// transaction resource consumption. @@ -60,28 +63,46 @@ impl Sum for Gas { /// These prices have an implicit denominator of 1,000 relative to the base unit /// of the staking token, so gas price 1,000 times 1 unit of gas is 1 base unit /// of staking token. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(try_from = "pb::GasPrices", into = "pb::GasPrices")] pub struct GasPrices { + pub asset_id: asset::Id, pub block_space_price: u64, pub compact_block_space_price: u64, pub verification_price: u64, pub execution_price: u64, } +impl Default for GasPrices { + fn default() -> Self { + Self { + asset_id: *STAKING_TOKEN_ASSET_ID, + block_space_price: 0, + compact_block_space_price: 0, + verification_price: 0, + execution_price: 0, + } + } +} + impl GasPrices { pub fn zero() -> Self { Self::default() } /// Use these gas prices to calculate the fee for a given gas vector. - pub fn fee(&self, gas: &Gas) -> Amount { - Amount::from( + pub fn fee(&self, gas: &Gas) -> Fee { + let amount = Amount::from( (self.block_space_price * gas.block_space) / 1_000 + (self.compact_block_space_price * gas.compact_block_space) / 1_000 + (self.verification_price * gas.verification) / 1_000 + (self.execution_price * gas.execution) / 1_000, - ) + ); + + Fee(Value { + asset_id: self.asset_id, + amount, + }) } } @@ -92,6 +113,12 @@ impl DomainType for GasPrices { impl From for pb::GasPrices { fn from(prices: GasPrices) -> Self { pb::GasPrices { + // Skip serializing the asset ID if it's the staking token. + asset_id: if prices.asset_id == *STAKING_TOKEN_ASSET_ID { + None + } else { + Some(prices.asset_id.into()) + }, block_space_price: prices.block_space_price, compact_block_space_price: prices.compact_block_space_price, verification_price: prices.verification_price, @@ -109,6 +136,11 @@ impl TryFrom for GasPrices { compact_block_space_price: proto.compact_block_space_price, verification_price: proto.verification_price, execution_price: proto.execution_price, + asset_id: proto + .asset_id + .map(TryInto::try_into) + .transpose()? + .unwrap_or_else(|| *STAKING_TOKEN_ASSET_ID), }) } } diff --git a/crates/core/component/fee/src/params.rs b/crates/core/component/fee/src/params.rs index 75b76b8f35..5e7dd78c33 100644 --- a/crates/core/component/fee/src/params.rs +++ b/crates/core/component/fee/src/params.rs @@ -9,6 +9,7 @@ use crate::GasPrices; #[serde(try_from = "pb::FeeParameters", into = "pb::FeeParameters")] pub struct FeeParameters { pub fixed_gas_prices: GasPrices, + pub fixed_alt_gas_prices: Vec, } impl DomainType for FeeParameters { @@ -21,6 +22,11 @@ impl TryFrom for FeeParameters { fn try_from(msg: pb::FeeParameters) -> anyhow::Result { Ok(FeeParameters { fixed_gas_prices: msg.fixed_gas_prices.unwrap_or_default().try_into()?, + fixed_alt_gas_prices: msg + .fixed_alt_gas_prices + .into_iter() + .map(|p| p.try_into()) + .collect::>()?, }) } } @@ -29,6 +35,11 @@ impl From for pb::FeeParameters { fn from(params: FeeParameters) -> Self { pb::FeeParameters { fixed_gas_prices: Some(params.fixed_gas_prices.into()), + fixed_alt_gas_prices: params + .fixed_alt_gas_prices + .into_iter() + .map(Into::into) + .collect(), } } } diff --git a/crates/proto/src/gen/penumbra.core.component.compact_block.v1.rs b/crates/proto/src/gen/penumbra.core.component.compact_block.v1.rs index 92edba517e..08d2c5abf6 100644 --- a/crates/proto/src/gen/penumbra.core.component.compact_block.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.compact_block.v1.rs @@ -39,6 +39,9 @@ pub struct CompactBlock { /// Updated gas prices, if they have changed. #[prost(message, optional, tag = "10")] pub gas_prices: ::core::option::Option, + /// Updated gas prices for alternative fee tokens, if they have changed. + #[prost(message, repeated, tag = "100")] + pub alt_gas_prices: ::prost::alloc::vec::Vec, /// The epoch index #[prost(uint64, tag = "11")] pub epoch_index: u64, diff --git a/crates/proto/src/gen/penumbra.core.component.compact_block.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.compact_block.v1.serde.rs index 8f49ca36c7..0f4dee3282 100644 --- a/crates/proto/src/gen/penumbra.core.component.compact_block.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.compact_block.v1.serde.rs @@ -36,6 +36,9 @@ impl serde::Serialize for CompactBlock { if self.gas_prices.is_some() { len += 1; } + if !self.alt_gas_prices.is_empty() { + len += 1; + } if self.epoch_index != 0 { len += 1; } @@ -71,6 +74,9 @@ impl serde::Serialize for CompactBlock { if let Some(v) = self.gas_prices.as_ref() { struct_ser.serialize_field("gasPrices", v)?; } + if !self.alt_gas_prices.is_empty() { + struct_ser.serialize_field("altGasPrices", &self.alt_gas_prices)?; + } if self.epoch_index != 0 { #[allow(clippy::needless_borrow)] struct_ser.serialize_field("epochIndex", ToString::to_string(&self.epoch_index).as_str())?; @@ -103,6 +109,8 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { "appParametersUpdated", "gas_prices", "gasPrices", + "alt_gas_prices", + "altGasPrices", "epoch_index", "epochIndex", ]; @@ -119,6 +127,7 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { SwapOutputs, AppParametersUpdated, GasPrices, + AltGasPrices, EpochIndex, __SkipField__, } @@ -152,6 +161,7 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { "swapOutputs" | "swap_outputs" => Ok(GeneratedField::SwapOutputs), "appParametersUpdated" | "app_parameters_updated" => Ok(GeneratedField::AppParametersUpdated), "gasPrices" | "gas_prices" => Ok(GeneratedField::GasPrices), + "altGasPrices" | "alt_gas_prices" => Ok(GeneratedField::AltGasPrices), "epochIndex" | "epoch_index" => Ok(GeneratedField::EpochIndex), _ => Ok(GeneratedField::__SkipField__), } @@ -182,6 +192,7 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { let mut swap_outputs__ = None; let mut app_parameters_updated__ = None; let mut gas_prices__ = None; + let mut alt_gas_prices__ = None; let mut epoch_index__ = None; while let Some(k) = map_.next_key()? { match k { @@ -247,6 +258,12 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { } gas_prices__ = map_.next_value()?; } + GeneratedField::AltGasPrices => { + if alt_gas_prices__.is_some() { + return Err(serde::de::Error::duplicate_field("altGasPrices")); + } + alt_gas_prices__ = Some(map_.next_value()?); + } GeneratedField::EpochIndex => { if epoch_index__.is_some() { return Err(serde::de::Error::duplicate_field("epochIndex")); @@ -271,6 +288,7 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { swap_outputs: swap_outputs__.unwrap_or_default(), app_parameters_updated: app_parameters_updated__.unwrap_or_default(), gas_prices: gas_prices__, + alt_gas_prices: alt_gas_prices__.unwrap_or_default(), epoch_index: epoch_index__.unwrap_or_default(), }) } diff --git a/crates/proto/src/gen/penumbra.core.component.fee.v1.rs b/crates/proto/src/gen/penumbra.core.component.fee.v1.rs index c478301eb9..9af646bae8 100644 --- a/crates/proto/src/gen/penumbra.core.component.fee.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.fee.v1.rs @@ -20,6 +20,11 @@ impl ::prost::Name for Fee { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GasPrices { + /// The asset ID of the fee token these prices are for. + /// + /// If absent, specifies the staking token implicitly. + #[prost(message, optional, tag = "15")] + pub asset_id: ::core::option::Option, /// The price per unit block space in terms of the staking token, with an implicit 1,000 denominator. #[prost(uint64, tag = "1")] pub block_space_price: u64, @@ -104,11 +109,23 @@ impl ::prost::Name for FeeTier { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct FeeParameters { - /// Fixed gas prices used to compute transactions' base fees. + /// Fixed gas prices in the native token used to compute transactions' base + /// fees. /// - /// In the future, this should be removed and replaced with parameters for dynamic gas pricing. + /// In the future, this should be removed and replaced with parameters for + /// dynamic gas pricing. #[prost(message, optional, tag = "1")] pub fixed_gas_prices: ::core::option::Option, + /// Fixed gas prices in other tokens used to compute transactions' base fees. + /// + /// In the future, this should be removed and replaced with fixed multiples of + /// the native token's price (so that there is one set of dynamically + /// determined gas prices in the native token, and derived gas prices in other + /// alternative tokens). + /// + /// If this is empty, no other tokens are accepted for gas. + #[prost(message, repeated, tag = "2")] + pub fixed_alt_gas_prices: ::prost::alloc::vec::Vec, } impl ::prost::Name for FeeParameters { const NAME: &'static str = "FeeParameters"; @@ -145,9 +162,12 @@ impl ::prost::Name for CurrentGasPricesRequest { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CurrentGasPricesResponse { - /// The current gas prices. + /// The current gas prices, in the preferred (native) token. #[prost(message, optional, tag = "1")] pub gas_prices: ::core::option::Option, + /// Other gas prices for other accepted tokens. + #[prost(message, repeated, tag = "2")] + pub alt_gas_prices: ::prost::alloc::vec::Vec, } impl ::prost::Name for CurrentGasPricesResponse { const NAME: &'static str = "CurrentGasPricesResponse"; diff --git a/crates/proto/src/gen/penumbra.core.component.fee.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.fee.v1.serde.rs index 844b950a8d..feabfe80a2 100644 --- a/crates/proto/src/gen/penumbra.core.component.fee.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.fee.v1.serde.rs @@ -81,10 +81,16 @@ impl serde::Serialize for CurrentGasPricesResponse { if self.gas_prices.is_some() { len += 1; } + if !self.alt_gas_prices.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.fee.v1.CurrentGasPricesResponse", len)?; if let Some(v) = self.gas_prices.as_ref() { struct_ser.serialize_field("gasPrices", v)?; } + if !self.alt_gas_prices.is_empty() { + struct_ser.serialize_field("altGasPrices", &self.alt_gas_prices)?; + } struct_ser.end() } } @@ -97,11 +103,14 @@ impl<'de> serde::Deserialize<'de> for CurrentGasPricesResponse { const FIELDS: &[&str] = &[ "gas_prices", "gasPrices", + "alt_gas_prices", + "altGasPrices", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { GasPrices, + AltGasPrices, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -125,6 +134,7 @@ impl<'de> serde::Deserialize<'de> for CurrentGasPricesResponse { { match value { "gasPrices" | "gas_prices" => Ok(GeneratedField::GasPrices), + "altGasPrices" | "alt_gas_prices" => Ok(GeneratedField::AltGasPrices), _ => Ok(GeneratedField::__SkipField__), } } @@ -145,6 +155,7 @@ impl<'de> serde::Deserialize<'de> for CurrentGasPricesResponse { V: serde::de::MapAccess<'de>, { let mut gas_prices__ = None; + let mut alt_gas_prices__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::GasPrices => { @@ -153,6 +164,12 @@ impl<'de> serde::Deserialize<'de> for CurrentGasPricesResponse { } gas_prices__ = map_.next_value()?; } + GeneratedField::AltGasPrices => { + if alt_gas_prices__.is_some() { + return Err(serde::de::Error::duplicate_field("altGasPrices")); + } + alt_gas_prices__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -160,6 +177,7 @@ impl<'de> serde::Deserialize<'de> for CurrentGasPricesResponse { } Ok(CurrentGasPricesResponse { gas_prices: gas_prices__, + alt_gas_prices: alt_gas_prices__.unwrap_or_default(), }) } } @@ -290,10 +308,16 @@ impl serde::Serialize for FeeParameters { if self.fixed_gas_prices.is_some() { len += 1; } + if !self.fixed_alt_gas_prices.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.fee.v1.FeeParameters", len)?; if let Some(v) = self.fixed_gas_prices.as_ref() { struct_ser.serialize_field("fixedGasPrices", v)?; } + if !self.fixed_alt_gas_prices.is_empty() { + struct_ser.serialize_field("fixedAltGasPrices", &self.fixed_alt_gas_prices)?; + } struct_ser.end() } } @@ -306,11 +330,14 @@ impl<'de> serde::Deserialize<'de> for FeeParameters { const FIELDS: &[&str] = &[ "fixed_gas_prices", "fixedGasPrices", + "fixed_alt_gas_prices", + "fixedAltGasPrices", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { FixedGasPrices, + FixedAltGasPrices, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -334,6 +361,7 @@ impl<'de> serde::Deserialize<'de> for FeeParameters { { match value { "fixedGasPrices" | "fixed_gas_prices" => Ok(GeneratedField::FixedGasPrices), + "fixedAltGasPrices" | "fixed_alt_gas_prices" => Ok(GeneratedField::FixedAltGasPrices), _ => Ok(GeneratedField::__SkipField__), } } @@ -354,6 +382,7 @@ impl<'de> serde::Deserialize<'de> for FeeParameters { V: serde::de::MapAccess<'de>, { let mut fixed_gas_prices__ = None; + let mut fixed_alt_gas_prices__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::FixedGasPrices => { @@ -362,6 +391,12 @@ impl<'de> serde::Deserialize<'de> for FeeParameters { } fixed_gas_prices__ = map_.next_value()?; } + GeneratedField::FixedAltGasPrices => { + if fixed_alt_gas_prices__.is_some() { + return Err(serde::de::Error::duplicate_field("fixedAltGasPrices")); + } + fixed_alt_gas_prices__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -369,6 +404,7 @@ impl<'de> serde::Deserialize<'de> for FeeParameters { } Ok(FeeParameters { fixed_gas_prices: fixed_gas_prices__, + fixed_alt_gas_prices: fixed_alt_gas_prices__.unwrap_or_default(), }) } } @@ -558,6 +594,9 @@ impl serde::Serialize for GasPrices { { use serde::ser::SerializeStruct; let mut len = 0; + if self.asset_id.is_some() { + len += 1; + } if self.block_space_price != 0 { len += 1; } @@ -571,6 +610,9 @@ impl serde::Serialize for GasPrices { len += 1; } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.fee.v1.GasPrices", len)?; + if let Some(v) = self.asset_id.as_ref() { + struct_ser.serialize_field("assetId", v)?; + } if self.block_space_price != 0 { #[allow(clippy::needless_borrow)] struct_ser.serialize_field("blockSpacePrice", ToString::to_string(&self.block_space_price).as_str())?; @@ -597,6 +639,8 @@ impl<'de> serde::Deserialize<'de> for GasPrices { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "asset_id", + "assetId", "block_space_price", "blockSpacePrice", "compact_block_space_price", @@ -609,6 +653,7 @@ impl<'de> serde::Deserialize<'de> for GasPrices { #[allow(clippy::enum_variant_names)] enum GeneratedField { + AssetId, BlockSpacePrice, CompactBlockSpacePrice, VerificationPrice, @@ -635,6 +680,7 @@ impl<'de> serde::Deserialize<'de> for GasPrices { E: serde::de::Error, { match value { + "assetId" | "asset_id" => Ok(GeneratedField::AssetId), "blockSpacePrice" | "block_space_price" => Ok(GeneratedField::BlockSpacePrice), "compactBlockSpacePrice" | "compact_block_space_price" => Ok(GeneratedField::CompactBlockSpacePrice), "verificationPrice" | "verification_price" => Ok(GeneratedField::VerificationPrice), @@ -658,12 +704,19 @@ impl<'de> serde::Deserialize<'de> for GasPrices { where V: serde::de::MapAccess<'de>, { + let mut asset_id__ = None; let mut block_space_price__ = None; let mut compact_block_space_price__ = None; let mut verification_price__ = None; let mut execution_price__ = None; while let Some(k) = map_.next_key()? { match k { + GeneratedField::AssetId => { + if asset_id__.is_some() { + return Err(serde::de::Error::duplicate_field("assetId")); + } + asset_id__ = map_.next_value()?; + } GeneratedField::BlockSpacePrice => { if block_space_price__.is_some() { return Err(serde::de::Error::duplicate_field("blockSpacePrice")); @@ -702,6 +755,7 @@ impl<'de> serde::Deserialize<'de> for GasPrices { } } Ok(GasPrices { + asset_id: asset_id__, block_space_price: block_space_price__.unwrap_or_default(), compact_block_space_price: compact_block_space_price__.unwrap_or_default(), verification_price: verification_price__.unwrap_or_default(), diff --git a/crates/proto/src/gen/penumbra.view.v1.rs b/crates/proto/src/gen/penumbra.view.v1.rs index e8857b101a..1591406240 100644 --- a/crates/proto/src/gen/penumbra.view.v1.rs +++ b/crates/proto/src/gen/penumbra.view.v1.rs @@ -1062,10 +1062,16 @@ impl ::prost::Name for GasPricesRequest { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GasPricesResponse { + /// The current gas prices, in the preferred (native) token. #[prost(message, optional, tag = "1")] pub gas_prices: ::core::option::Option< super::super::core::component::fee::v1::GasPrices, >, + /// Other gas prices for other accepted tokens. + #[prost(message, repeated, tag = "2")] + pub alt_gas_prices: ::prost::alloc::vec::Vec< + super::super::core::component::fee::v1::GasPrices, + >, } impl ::prost::Name for GasPricesResponse { const NAME: &'static str = "GasPricesResponse"; diff --git a/crates/proto/src/gen/penumbra.view.v1.serde.rs b/crates/proto/src/gen/penumbra.view.v1.serde.rs index 79c50f2faa..6640e583bf 100644 --- a/crates/proto/src/gen/penumbra.view.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.view.v1.serde.rs @@ -2954,10 +2954,16 @@ impl serde::Serialize for GasPricesResponse { if self.gas_prices.is_some() { len += 1; } + if !self.alt_gas_prices.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.GasPricesResponse", len)?; if let Some(v) = self.gas_prices.as_ref() { struct_ser.serialize_field("gasPrices", v)?; } + if !self.alt_gas_prices.is_empty() { + struct_ser.serialize_field("altGasPrices", &self.alt_gas_prices)?; + } struct_ser.end() } } @@ -2970,11 +2976,14 @@ impl<'de> serde::Deserialize<'de> for GasPricesResponse { const FIELDS: &[&str] = &[ "gas_prices", "gasPrices", + "alt_gas_prices", + "altGasPrices", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { GasPrices, + AltGasPrices, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -2998,6 +3007,7 @@ impl<'de> serde::Deserialize<'de> for GasPricesResponse { { match value { "gasPrices" | "gas_prices" => Ok(GeneratedField::GasPrices), + "altGasPrices" | "alt_gas_prices" => Ok(GeneratedField::AltGasPrices), _ => Ok(GeneratedField::__SkipField__), } } @@ -3018,6 +3028,7 @@ impl<'de> serde::Deserialize<'de> for GasPricesResponse { V: serde::de::MapAccess<'de>, { let mut gas_prices__ = None; + let mut alt_gas_prices__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::GasPrices => { @@ -3026,6 +3037,12 @@ impl<'de> serde::Deserialize<'de> for GasPricesResponse { } gas_prices__ = map_.next_value()?; } + GeneratedField::AltGasPrices => { + if alt_gas_prices__.is_some() { + return Err(serde::de::Error::duplicate_field("altGasPrices")); + } + alt_gas_prices__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -3033,6 +3050,7 @@ impl<'de> serde::Deserialize<'de> for GasPricesResponse { } Ok(GasPricesResponse { gas_prices: gas_prices__, + alt_gas_prices: alt_gas_prices__.unwrap_or_default(), }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index af13fbdf48..0760acde5a 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/crates/view/src/planner/action_list.rs b/crates/view/src/planner/action_list.rs index 1596d0180f..b0c8b3cb2a 100644 --- a/crates/view/src/planner/action_list.rs +++ b/crates/view/src/planner/action_list.rs @@ -123,7 +123,7 @@ impl ActionList { /// estimated, because the actual base fee paid by the transaction will /// depend on the gas prices at the time it's accepted on-chain. fn compute_fee_estimate(&self, gas_prices: &GasPrices, fee_tier: &FeeTier) -> Fee { - let base_fee = Fee::from_staking_token_amount(gas_prices.fee(&self.gas_cost())); + let base_fee = gas_prices.fee(&self.gas_cost()); base_fee.apply_tier(*fee_tier) } diff --git a/crates/view/src/service.rs b/crates/view/src/service.rs index 5a2b078549..a074b0845a 100644 --- a/crates/view/src/service.rs +++ b/crates/view/src/service.rs @@ -1606,6 +1606,7 @@ impl ViewService for ViewServer { let response = GasPricesResponse { gas_prices: Some(gas_prices.into()), + alt_gas_prices: Vec::new(), }; Ok(tonic::Response::new(response)) diff --git a/proto/penumbra/penumbra/core/component/compact_block/v1/compact_block.proto b/proto/penumbra/penumbra/core/component/compact_block/v1/compact_block.proto index 83bb115d34..e86e33e9ec 100644 --- a/proto/penumbra/penumbra/core/component/compact_block/v1/compact_block.proto +++ b/proto/penumbra/penumbra/core/component/compact_block/v1/compact_block.proto @@ -28,6 +28,8 @@ message CompactBlock { bool app_parameters_updated = 9; // Updated gas prices, if they have changed. fee.v1.GasPrices gas_prices = 10; + // Updated gas prices for alternative fee tokens, if they have changed. + repeated fee.v1.GasPrices alt_gas_prices = 100; // The epoch index uint64 epoch_index = 11; } diff --git a/proto/penumbra/penumbra/core/component/fee/v1/fee.proto b/proto/penumbra/penumbra/core/component/fee/v1/fee.proto index 14a5f6931e..14a6c258aa 100644 --- a/proto/penumbra/penumbra/core/component/fee/v1/fee.proto +++ b/proto/penumbra/penumbra/core/component/fee/v1/fee.proto @@ -14,6 +14,11 @@ message Fee { } message GasPrices { + // The asset ID of the fee token these prices are for. + // + // If absent, specifies the staking token implicitly. + asset.v1.AssetId asset_id = 15; + // The price per unit block space in terms of the staking token, with an implicit 1,000 denominator. uint64 block_space_price = 1; // The price per unit compact block space in terms of the staking token, with an implicit 1,000 denominator. @@ -39,10 +44,22 @@ message FeeTier { // Fee component configuration data. message FeeParameters { - // Fixed gas prices used to compute transactions' base fees. + // Fixed gas prices in the native token used to compute transactions' base + // fees. // - // In the future, this should be removed and replaced with parameters for dynamic gas pricing. + // In the future, this should be removed and replaced with parameters for + // dynamic gas pricing. GasPrices fixed_gas_prices = 1; + + // Fixed gas prices in other tokens used to compute transactions' base fees. + // + // In the future, this should be removed and replaced with fixed multiples of + // the native token's price (so that there is one set of dynamically + // determined gas prices in the native token, and derived gas prices in other + // alternative tokens). + // + // If this is empty, no other tokens are accepted for gas. + repeated GasPrices fixed_alt_gas_prices = 2; } // Fee-specific genesis content. @@ -60,6 +77,8 @@ service QueryService { message CurrentGasPricesRequest {} message CurrentGasPricesResponse { - // The current gas prices. + // The current gas prices, in the preferred (native) token. GasPrices gas_prices = 1; + // Other gas prices for other accepted tokens. + repeated GasPrices alt_gas_prices = 2; } diff --git a/proto/penumbra/penumbra/view/v1/view.proto b/proto/penumbra/penumbra/view/v1/view.proto index 4cb9d8e19e..76aeaea5b2 100644 --- a/proto/penumbra/penumbra/view/v1/view.proto +++ b/proto/penumbra/penumbra/view/v1/view.proto @@ -523,7 +523,10 @@ message AppParametersResponse { message GasPricesRequest {} message GasPricesResponse { + // The current gas prices, in the preferred (native) token. core.component.fee.v1.GasPrices gas_prices = 1; + // Other gas prices for other accepted tokens. + repeated core.component.fee.v1.GasPrices alt_gas_prices = 2; } // Requests the current FMD parameters from the view service.