From a0baeda259f7a6b3b4340d1cd985c5fbb6b55e63 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Sat, 4 May 2024 22:30:14 -0700 Subject: [PATCH] fee: support v0 multi-asset fees (protocol side only) This commit implements a v0 of multi-asset fees (on the protocol side). Multi-asset fees are important because they allows users who send tokens into Penumbra over IBC to start using the chain immediately. It does this by extending the `GasPrices` struct to include the asset ID of the fee token the prices are for. This allows natural handling of multi-asset fees by just having multiple sets of `GasPrices`. In the past, we've laid the groundwork for this (by allowing other assets in the `Fee` structure, even if it defaults to the staking token) but haven't prioritized it, due to concerns about some of the details of the implementation, particularly: A: How does the chain know that the prices of non-native fee tokens are correct? B: What happens to the non-native fee tokens? C: What are the privacy implications for users paying with non-native fee tokens? This design punts on (A) by only allowing non-native fee tokens with prices explicitly specified using governance. In the future, Penumbra could own DEX as a price oracle, but this would be better to do after it's live and we have real data about it, rather than worrying about edge cases now (e.g., if someone can temporarily manipulate a price for their own trades, that's not a big deal in itself, but hooking it to the fee mechanism used for resource usage could make it one). The approach to (C) is that these explicitly specified prices should be substantially higher (10x) than those of the native token. This means that regardless of price variability, it should remain the case that the native fee token is cheaper, so that the majority of users will use the same fee token and their transactions will not be distinguishable. On the other hand, letting it be _possible_ to use non native fee tokens means that users who send IBC tokens into Penumbra can access the protocol, and even if one of their first acts is to swap some de minimis amount of the native token, they can do that within the system. This implementation currently does not properly handle (B), in that it silently burns all fees, regardless of token. It is not appropriate to burn any tokens other than the native token, whose supply is managed by the chain. Instead, the transaction-level fee check should move into `check_and_execute`, and call a method on the fee component that records the fee amount in the object store. At the end of the block, all non-native fees should be swapped into the native token and then burned. Emitted events should record: - an `EventFee` for each transaction, with the transaction's gas cost, the gas prices used to compute the base fee, the base fee, and the actual fee; - an `EventFeeBurn` at the end of the block, with the total amount of native fees burned as well as, for each alt fee token used to pay fees in that block, how much of the alt token was paid and how much of the native token it was traded for. Client support is still mostly missing, though I have manually tested this on a local devnet, by 1. Creating the devnet 2. Executing a parameter change proposal to set `dexParams.fixedAltGasPrices` to `"[{\"assetId\":{\"altBaseDenom\":\"ugm\"},\"blockSpacePrice\":\"80000\",\"compactBlockSpacePrice\":\"80000\",\"verificationPrice\":\"80000\",\"executionPrice\":\"80000\"}]"` 3. Hardcoding at the top of `TxCmd::exec` a change to the `gas_prices` to add the `gm` asset ID and multiplying all the prices by 10. To properly support clients, we need to - Record the `gas_prices` data for alt fee tokens that is newly included in the `CompactBlock` in the view server's storage (probably in a new `gas_prices` table that maps a fee token asset ID to the latest price); - Use that data to populate the view server's gas prices RPC response (instead of the current `Vec::new` stub that reports any alt fee tokens as non-existing); - Change the `pcli tx` command so that the existing `--fee-tier` and a new `--fee-token` parameters move to `pcli tx`, and apply to all subcommands (i.e., `pcli tx --fee-tier ... --fee-token gm send ...`) so that `TxCmd::exec` can pull the right `GasPrices` out of the RPC response. --- crates/bin/pcli/src/command/tx.rs | 29 +++---- crates/bin/pcli/src/network.rs | 22 +---- crates/bin/pd/src/testnet/generate.rs | 18 ++++ .../action_handler/transaction/stateful.rs | 81 +++++++++++------- crates/core/app/src/params/change.rs | 16 ++-- crates/core/asset/src/asset/registry.rs | 19 ++-- .../compact-block/src/compact_block.rs | 12 ++- .../compact-block/src/component/manager.rs | 16 ++-- crates/core/component/fee/src/component.rs | 2 - .../core/component/fee/src/component/rpc.rs | 1 + .../core/component/fee/src/component/view.rs | 36 +++++--- crates/core/component/fee/src/gas.rs | 40 ++++++++- crates/core/component/fee/src/params.rs | 11 +++ ...enumbra.core.component.compact_block.v1.rs | 3 + ...a.core.component.compact_block.v1.serde.rs | 18 ++++ .../src/gen/penumbra.core.component.fee.v1.rs | 26 +++++- .../penumbra.core.component.fee.v1.serde.rs | 54 ++++++++++++ crates/proto/src/gen/penumbra.view.v1.rs | 6 ++ .../proto/src/gen/penumbra.view.v1.serde.rs | 18 ++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 405981 -> 407483 bytes crates/view/src/planner/action_list.rs | 2 +- crates/view/src/service.rs | 1 + .../compact_block/v1/compact_block.proto | 2 + .../penumbra/core/component/fee/v1/fee.proto | 25 +++++- proto/penumbra/penumbra/view/v1/view.proto | 3 + 25 files changed, 346 insertions(+), 115 deletions(-) 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 6a5a5c4fdb..df65858396 100644 --- a/crates/core/app/src/action_handler/transaction/stateful.rs +++ b/crates/core/app/src/action_handler/transaction/stateful.rs @@ -72,37 +72,6 @@ pub async fn expiry_height_is_valid(state: S, expiry_height: u64) Ok(()) } -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()); - 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(); - - 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 - ); - - // We split the check to provide granular error messages. - 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 - ); - - Ok(()) -} - pub async fn fmd_parameters_valid(state: S, transaction: &Transaction) -> Result<()> { let previous_fmd_parameters = state .get_previous_fmd_parameters() @@ -171,3 +140,53 @@ pub async fn claimed_anchor_is_valid( ) -> Result<()> { state.check_claimed_anchor(transaction.anchor).await } + +pub async fn fee_greater_than_base_fee( + state: S, + transaction: &Transaction, +) -> Result<()> { + // 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 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!( + 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, + ); + + let transaction_base_fee = current_gas_prices.fee(&transaction.gas_cost()); + + ensure!( + 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 af13fbdf48e5594ee61379afab1aff3306f5e87c..0760acde5a840e0b60d6d82a8b1e44b68ded99c8 100644 GIT binary patch delta 7328 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 delta 6259 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 NQ5!a1d;PV!{{e-r?$`hT 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.