From 5e23b959bbb63a14f40c8f31b1644439569b1d57 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Tue, 25 Jun 2024 09:35:04 -0700 Subject: [PATCH] dex: Swap alt fee tokens for native fee token (#4643) ## Describe your changes Moves fee handling out of the Transaction's ActionHandler and into the component, which accumulates fees (in any token), and adds hooks to the DEX thatadd chain-submitted swaps for any non-native fee tokens into the native token. These are then accumulated back into the fee component. The base fee and tip are tracked separately through this whole process, in order to support the intended behaviour (the base fee is burned to account for the _chain_'s resource use, the tip is sent to the proposer to encourage them to include transactions in fee priority order). However, tip handling is not currently implemented, so both are burned. Later, the funding/distribution components should extract the tip and send it to the proposer's funding streams. However, until a robust fee market develops, this form of proposer incentivization can be deferred. The accounting will be there for it when it arises. ## Issue ticket number and link #4328 ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > consensus-breaking but not state-breaking, no migrations necessary --------- Co-authored-by: Erwan Or --- Cargo.lock | 1 + .../app/src/action_handler/transaction.rs | 25 +- .../action_handler/transaction/stateful.rs | 58 +-- crates/core/app/src/app/mod.rs | 12 +- crates/core/app/tests/app_check_dex_vcb.rs | 75 +++ .../dex/src/component/action_handler/swap.rs | 16 +- .../component/dex/src/component/chandelier.rs | 10 +- .../dex/src/component/circuit_breaker/mod.rs | 1 + .../src/component/circuit_breaker/value.rs | 20 +- .../core/component/dex/src/component/dex.rs | 107 ++++- .../core/component/dex/src/component/flow.rs | 6 + .../core/component/dex/src/component/mod.rs | 1 + .../src/component/router/route_and_fill.rs | 4 +- .../dex/src/component/router/tests.rs | 4 +- .../dex/src/component/swap_manager.rs | 27 +- .../core/component/dex/src/component/tests.rs | 4 +- crates/core/component/fee/Cargo.toml | 1 + crates/core/component/fee/src/component.rs | 32 +- .../component/fee/src/component/fee_pay.rs | 77 +++ .../core/component/fee/src/component/view.rs | 44 +- crates/core/component/fee/src/gas.rs | 31 +- crates/core/component/fee/src/state_key.rs | 4 + .../src/gen/penumbra.core.component.fee.v1.rs | 71 +++ .../penumbra.core.component.fee.v1.serde.rs | 440 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 524518 -> 526509 bytes .../penumbra/core/component/fee/v1/fee.proto | 36 ++ 26 files changed, 992 insertions(+), 115 deletions(-) create mode 100644 crates/core/app/tests/app_check_dex_vcb.rs create mode 100644 crates/core/component/fee/src/component/fee_pay.rs diff --git a/Cargo.lock b/Cargo.lock index db10dbbc5f..f49771ca27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5025,6 +5025,7 @@ dependencies = [ "cnidarium-component", "decaf377 0.5.0", "decaf377-rdsa", + "im", "metrics", "penumbra-asset", "penumbra-num", diff --git a/crates/core/app/src/action_handler/transaction.rs b/crates/core/app/src/action_handler/transaction.rs index b8d2bb15a9..5bb585dfdc 100644 --- a/crates/core/app/src/action_handler/transaction.rs +++ b/crates/core/app/src/action_handler/transaction.rs @@ -3,9 +3,10 @@ use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; +use penumbra_fee::component::FeePay as _; use penumbra_sct::{component::source::SourceContext, CommitmentSource}; use penumbra_shielded_pool::component::ClueManager; -use penumbra_transaction::Transaction; +use penumbra_transaction::{gas::GasCost as _, Transaction}; use tokio::task::JoinSet; use tracing::{instrument, Instrument}; @@ -29,7 +30,20 @@ impl AppActionHandler for Transaction { // We only instrument the top-level `check_stateless`, so we get one span for each transaction. #[instrument(skip(self, _context))] async fn check_stateless(&self, _context: ()) -> Result<()> { + // This check should be done first, and complete before all other + // stateless checks, like proof verification. In addition to proving + // that value balances, the binding signature binds the proofs to the + // transaction, as the binding signature can only be created with + // knowledge of all of the openings to the commitments the transaction + // makes proofs against. (This is where the name binding signature comes + // from). + // + // This allows us to cheaply eliminate a large class of invalid + // transactions upfront -- past this point, we can be sure that the user + // who submitted the transaction actually formed the proofs, rather than + // replaying them from another transaction. valid_binding_signature(self)?; + // Other checks probably too cheap to be worth splitting into tasks. num_clues_equal_to_num_outputs(self)?; check_memo_exists_if_outputs_absent_if_not(self)?; @@ -59,8 +73,9 @@ impl AppActionHandler for Transaction { async fn check_historical(&self, state: Arc) -> Result<()> { let mut action_checks = JoinSet::new(); - // SAFETY: Transaction parameters (chain id, expiry height, fee) against chain state + // SAFETY: Transaction parameters (chain id, expiry height) against chain state // that cannot change during transaction execution. + // The fee is _not_ checked here, but during execution. tx_parameters_historical_check(state.clone(), self).await?; // SAFETY: anchors are historical data and cannot change during transaction execution. claimed_anchor_is_valid(state.clone(), self).await?; @@ -95,6 +110,12 @@ impl AppActionHandler for Transaction { }; state.put_current_source(Some(source)); + // Check and record the transaction's fee payment, + // before doing the rest of execution. + let gas_used = self.gas_cost(); + let fee = self.transaction_body.transaction_parameters.fee; + state.pay_fee(gas_used, fee).await?; + for (i, action) in self.actions().enumerate() { let span = action.create_span(i); action diff --git a/crates/core/app/src/action_handler/transaction/stateful.rs b/crates/core/app/src/action_handler/transaction/stateful.rs index de5dbda678..6aac2c9d50 100644 --- a/crates/core/app/src/action_handler/transaction/stateful.rs +++ b/crates/core/app/src/action_handler/transaction/stateful.rs @@ -1,11 +1,9 @@ use anyhow::{ensure, Result}; use cnidarium::StateRead; -use penumbra_fee::component::StateReadExt as _; use penumbra_sct::component::clock::EpochRead; use penumbra_sct::component::tree::VerificationExt; use penumbra_shielded_pool::component::StateReadExt as _; use penumbra_shielded_pool::fmd; -use penumbra_transaction::gas::GasCost; use penumbra_transaction::{Transaction, TransactionParameters}; use crate::app::StateReadExt; @@ -17,8 +15,7 @@ pub async fn tx_parameters_historical_check( let TransactionParameters { chain_id, expiry_height, - // This is checked in `fee_greater_than_base_fee` against the whole - // transaction, for convenience. + // This is checked during execution. fee: _, // IMPORTANT: Adding a transaction parameter? Then you **must** add a SAFETY // argument here to justify why it is safe to validate against a historical @@ -31,9 +28,6 @@ pub async fn tx_parameters_historical_check( // SAFETY: This is safe to do in a **historical** check because the chain's current // block height cannot change during transaction processing. expiry_height_is_valid(&state, expiry_height).await?; - // SAFETY: This is safe to do in a **historical** check as long as the current gas prices - // are static, or set in the previous block. - fee_greater_than_base_fee(&state, transaction).await?; Ok(()) } @@ -145,53 +139,3 @@ 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/app/mod.rs b/crates/core/app/src/app/mod.rs index 31f41fcde0..dab4f11a37 100644 --- a/crates/core/app/src/app/mod.rs +++ b/crates/core/app/src/app/mod.rs @@ -15,7 +15,7 @@ use penumbra_compact_block::component::CompactBlockManager; use penumbra_dex::component::StateReadExt as _; use penumbra_dex::component::{Dex, StateWriteExt as _}; use penumbra_distributions::component::{Distributions, StateReadExt as _, StateWriteExt as _}; -use penumbra_fee::component::{Fee, StateReadExt as _, StateWriteExt as _}; +use penumbra_fee::component::{FeeComponent, StateReadExt as _, StateWriteExt as _}; use penumbra_funding::component::Funding; use penumbra_funding::component::{StateReadExt as _, StateWriteExt as _}; use penumbra_governance::component::{Governance, StateReadExt as _, StateWriteExt as _}; @@ -139,7 +139,7 @@ impl App { CommunityPool::init_chain(&mut state_tx, Some(&genesis.community_pool_content)) .await; Governance::init_chain(&mut state_tx, Some(&genesis.governance_content)).await; - Fee::init_chain(&mut state_tx, Some(&genesis.fee_content)).await; + FeeComponent::init_chain(&mut state_tx, Some(&genesis.fee_content)).await; Funding::init_chain(&mut state_tx, Some(&genesis.funding_content)).await; state_tx @@ -155,7 +155,7 @@ impl App { Dex::init_chain(&mut state_tx, None).await; Governance::init_chain(&mut state_tx, None).await; CommunityPool::init_chain(&mut state_tx, None).await; - Fee::init_chain(&mut state_tx, None).await; + FeeComponent::init_chain(&mut state_tx, None).await; Funding::init_chain(&mut state_tx, None).await; } }; @@ -340,7 +340,7 @@ impl App { CommunityPool::begin_block(&mut arc_state_tx, begin_block).await; Governance::begin_block(&mut arc_state_tx, begin_block).await; Staking::begin_block(&mut arc_state_tx, begin_block).await; - Fee::begin_block(&mut arc_state_tx, begin_block).await; + FeeComponent::begin_block(&mut arc_state_tx, begin_block).await; Funding::begin_block(&mut arc_state_tx, begin_block).await; let state_tx = Arc::try_unwrap(arc_state_tx) @@ -471,7 +471,7 @@ impl App { CommunityPool::end_block(&mut arc_state_tx, end_block).await; Governance::end_block(&mut arc_state_tx, end_block).await; Staking::end_block(&mut arc_state_tx, end_block).await; - Fee::end_block(&mut arc_state_tx, end_block).await; + FeeComponent::end_block(&mut arc_state_tx, end_block).await; Funding::end_block(&mut arc_state_tx, end_block).await; let mut state_tx = Arc::try_unwrap(arc_state_tx) .expect("components did not retain copies of shared state"); @@ -533,7 +533,7 @@ impl App { Staking::end_epoch(&mut arc_state_tx) .await .expect("able to call end_epoch on Staking component"); - Fee::end_epoch(&mut arc_state_tx) + FeeComponent::end_epoch(&mut arc_state_tx) .await .expect("able to call end_epoch on Fee component"); Funding::end_epoch(&mut arc_state_tx) diff --git a/crates/core/app/tests/app_check_dex_vcb.rs b/crates/core/app/tests/app_check_dex_vcb.rs new file mode 100644 index 0000000000..266e15f4a7 --- /dev/null +++ b/crates/core/app/tests/app_check_dex_vcb.rs @@ -0,0 +1,75 @@ +mod common; + +use self::common::TempStorageExt; +use cnidarium::{ArcStateDeltaExt, StateDelta, TempStorage}; +use cnidarium_component::ActionHandler; +use penumbra_asset::asset; +use penumbra_dex::{ + component::ValueCircuitBreakerRead, + swap::{SwapPlaintext, SwapPlan}, + TradingPair, +}; +use penumbra_fee::Fee; +use penumbra_keys::{test_keys, Address}; +use penumbra_num::Amount; +use penumbra_sct::component::source::SourceContext; +use rand_core::SeedableRng; +use std::{ops::Deref, sync::Arc}; + +#[tokio::test] +/// Minimal reproduction of a bug in the DEX VCB swap flow tracking. +/// +/// Overview: The DEX VCB was double-counting swap flows for a same asset +/// by computing: `aggregate = (delta + aggregate) + aggregate`, instead +/// it should compute: `aggregate = delta + aggregate`. +/// This bug was fixed in #4643. +async fn dex_vcb_tracks_multiswap() -> anyhow::Result<()> { + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1776); + let storage = TempStorage::new().await?.apply_default_genesis().await?; + let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + + // Create the first swap: + let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); + let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); + let trading_pair = TradingPair::new(gm.id(), gn.id()); + + let delta_1 = Amount::from(100_000u64); + let delta_2 = Amount::from(0u64); + let fee = Fee::default(); + let claim_address: Address = test_keys::ADDRESS_0.deref().clone(); + let plaintext = + SwapPlaintext::new(&mut rng, trading_pair, delta_1, delta_2, fee, claim_address); + + let swap_plan = SwapPlan::new(&mut rng, plaintext.clone()); + let swap_one = swap_plan.swap(&test_keys::FULL_VIEWING_KEY); + + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx.put_mock_source(1u8); + swap_one.check_and_execute(&mut state_tx).await?; + + // Observe the DEX VCB has been credited: + let gm_vcb_amount = state_tx + .get_dex_vcb_for_asset(&gm.id()) + .await? + .expect("we just accumulated a swap"); + assert_eq!( + gm_vcb_amount, + 100_000u128.into(), + "the DEX VCB does not contain swap 1" + ); + + // Let's add another swap: + let swap_two = swap_one.clone(); + swap_two.check_and_execute(&mut state_tx).await?; + let gm_vcb_amount = state_tx + .get_dex_vcb_for_asset(&gm.id()) + .await? + .expect("we accumulated two swaps"); + assert_eq!( + gm_vcb_amount, + 200_000u128.into(), + "the DEX VCB does not contain swap 2" + ); + + Ok(()) +} diff --git a/crates/core/component/dex/src/component/action_handler/swap.rs b/crates/core/component/dex/src/component/action_handler/swap.rs index 63fd35ca7c..145262252e 100644 --- a/crates/core/component/dex/src/component/action_handler/swap.rs +++ b/crates/core/component/dex/src/component/action_handler/swap.rs @@ -9,7 +9,7 @@ use penumbra_proto::StateWriteProto; use penumbra_sct::component::source::SourceContext; use crate::{ - component::{InternalDexWrite, StateReadExt, SwapDataRead, SwapDataWrite, SwapManager}, + component::{InternalDexWrite, StateReadExt, SwapDataWrite, SwapManager}, event, swap::{proof::SwapProofPublic, Swap}, }; @@ -46,18 +46,10 @@ impl ActionHandler for Swap { let swap = self; - // All swaps will be tallied for the block so the - // BatchSwapOutputData for the trading pair/block height can - // be set during `end_block`. - let mut swap_flow = state.swap_flow(&swap.body.trading_pair); - - // Add the amount of each asset being swapped to the batch swap flow. - swap_flow.0 += swap.body.delta_1_i; - swap_flow.1 += swap.body.delta_2_i; - - // Set the batch swap flow for the trading pair. + // Accumulate the swap's flows, crediting the DEX VCB for the inflows. + let flow = (swap.body.delta_1_i, swap.body.delta_2_i); state - .put_swap_flow(&swap.body.trading_pair, swap_flow) + .accumulate_swap_flow(&swap.body.trading_pair, flow.into()) .await?; // Record the swap commitment in the state. diff --git a/crates/core/component/dex/src/component/chandelier.rs b/crates/core/component/dex/src/component/chandelier.rs index f8a819c650..40a4032f3c 100644 --- a/crates/core/component/dex/src/component/chandelier.rs +++ b/crates/core/component/dex/src/component/chandelier.rs @@ -328,10 +328,11 @@ mod tests { swap_flow.0 += 0u32.into(); swap_flow.1 += gn.value(1u32.into()).amount; - // Set the batch swap flow for the trading pair. + // Accumulate it into the batch swap flow for the trading pair. + // Since this is currently empty this is the same as setting it. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()) + .accumulate_swap_flow(&trading_pair, swap_flow.clone()) .await .unwrap(); @@ -420,10 +421,11 @@ mod tests { // Swap 2 gn into penumbra, meaning each position is filled. swap_flow.1 += gn.value(2u32.into()).amount; - // Set the batch swap flow for the trading pair. + // Accumulate it into the batch swap flow for the trading pair. + // Since this is currently empty this is the same as setting it. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()) + .accumulate_swap_flow(&trading_pair, swap_flow.clone()) .await .unwrap(); diff --git a/crates/core/component/dex/src/component/circuit_breaker/mod.rs b/crates/core/component/dex/src/component/circuit_breaker/mod.rs index 6cabc33168..6be2b259d3 100644 --- a/crates/core/component/dex/src/component/circuit_breaker/mod.rs +++ b/crates/core/component/dex/src/component/circuit_breaker/mod.rs @@ -3,3 +3,4 @@ pub(crate) mod value; pub(crate) use execution::ExecutionCircuitBreaker; pub(crate) use value::ValueCircuitBreaker; +pub use value::ValueCircuitBreakerRead; diff --git a/crates/core/component/dex/src/component/circuit_breaker/value.rs b/crates/core/component/dex/src/component/circuit_breaker/value.rs index d92228c885..4708da46b0 100644 --- a/crates/core/component/dex/src/component/circuit_breaker/value.rs +++ b/crates/core/component/dex/src/component/circuit_breaker/value.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; -use cnidarium::StateWrite; -use penumbra_asset::Value; +use cnidarium::{StateRead, StateWrite}; +use penumbra_asset::{asset, Value}; use penumbra_num::Amount; use penumbra_proto::{StateReadProto, StateWriteProto}; use tonic::async_trait; @@ -8,6 +8,16 @@ use tracing::instrument; use crate::{event, state_key}; +#[async_trait] +pub trait ValueCircuitBreakerRead: StateRead { + /// Fetch the DEX VCB balance for a specified asset id. + async fn get_dex_vcb_for_asset(&self, id: &asset::Id) -> Result> { + Ok(self.get(&state_key::value_balance(&id)).await?) + } +} + +impl ValueCircuitBreakerRead for T {} + /// Tracks the aggregate value of deposits in the DEX. #[async_trait] pub(crate) trait ValueCircuitBreaker: StateWrite { @@ -19,7 +29,7 @@ pub(crate) trait ValueCircuitBreaker: StateWrite { } let prev_balance: Amount = self - .get(&state_key::value_balance(&value.asset_id)) + .get_dex_vcb_for_asset(&value.asset_id) .await? .unwrap_or_default(); let new_balance = prev_balance @@ -41,7 +51,7 @@ pub(crate) trait ValueCircuitBreaker: StateWrite { } let prev_balance: Amount = self - .get(&state_key::value_balance(&value.asset_id)) + .get_dex_vcb_for_asset(&value.asset_id) .await? .unwrap_or_default(); let new_balance = prev_balance @@ -257,7 +267,7 @@ mod tests { // Set the batch swap flow for the trading pair. state_tx - .put_swap_flow(&trading_pair, swap_flow.clone()) + .accumulate_swap_flow(&trading_pair, swap_flow.clone()) .await .unwrap(); state_tx.apply(); diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index 56fe90ac8b..dfe982efb3 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; use anyhow::Result; @@ -7,6 +7,9 @@ use cnidarium::{StateRead, StateWrite}; use cnidarium_component::Component; use penumbra_asset::asset; use penumbra_asset::{Value, STAKING_TOKEN_ASSET_ID}; +use penumbra_fee::component::StateWriteExt as _; +use penumbra_fee::Fee; +use penumbra_num::Amount; use penumbra_proto::{StateReadProto, StateWriteProto}; use tendermint::v0_37::abci; use tracing::instrument; @@ -52,6 +55,50 @@ impl Component for Dex { state: &mut Arc, end_block: &abci::request::EndBlock, ) { + // F.0. Add all non-native fee payments as swap flows. + let base_fees_and_tips = { + let state_ref = + Arc::get_mut(state).expect("should have unique ref at start of Dex::end_block"); + + // Extract the accumulated base fees and tips from the fee component, leaving 0 in its place. + let base_fees_and_tips = state_ref.take_accumulated_base_fees_and_tips(); + + // For each nonnative fee asset, add it in as if it were a chain-submitted swap. + for (asset_id, (base_fee, tip)) in base_fees_and_tips.iter() { + if *asset_id == *STAKING_TOKEN_ASSET_ID { + continue; + } + let pair = TradingPair::new(*asset_id, *STAKING_TOKEN_ASSET_ID); + // We want to swap all of the fees into the native token, the base/tip distinction + // just affects where the resulting fees go. + let total = *base_fee + *tip; + // DANGEROUS: need to be careful about which side of the pair is which, + // but the existing API is unsafe and fixing it would be a much larger refactor. + let flow = if pair.asset_1() == *asset_id { + (total, Amount::zero()) + } else { + (Amount::zero(), total) + }; + tracing::debug!( + ?asset_id, + ?base_fee, + ?tip, + ?total, + ?flow, + "inserting chain-submitted swap for alt fee token" + ); + + // Accumulate into the swap flows for this block. + state_ref + .accumulate_swap_flow(&pair, flow.into()) + .await + .expect("should be able to credit DEX VCB"); + } + + // Hold on to the list of base fees and tips so we can claim outputs correctly. + base_fees_and_tips + }; + // 1. Add all newly opened positions to the DEX. // This has already happened in the action handlers for each `PositionOpen` action. @@ -63,9 +110,12 @@ impl Component for Dex { .expect("dex params are set") .max_execution_budget; + // Local cache of BSODs used for claiming fee swaps. + let mut bsods = BTreeMap::new(); + for (trading_pair, swap_flows) in state.swap_flows() { let batch_start = std::time::Instant::now(); - state + let bsod = state .handle_batch_swaps( trading_pair, swap_flows, @@ -83,6 +133,59 @@ impl Component for Dex { .expect("handling batch swaps is infaillible"); metrics::histogram!(crate::component::metrics::DEX_BATCH_DURATION) .record(batch_start.elapsed()); + + bsods.insert(trading_pair, bsod); + } + + // F.1. Having performed all batch swaps, "claim" the base fees and tips. + // The VCB has already been debited through the BSOD. + { + let state_ref = + Arc::get_mut(state).expect("should have unique ref after finishing batch swaps"); + for (asset_id, (base_fee, tip)) in base_fees_and_tips.iter() { + if *asset_id == *STAKING_TOKEN_ASSET_ID { + // In this case, there was nothing to swap, so there's nothing + // to claim and we just accumulate the fee we took back into the fee component. + state_ref.raw_accumulate_base_fee(Fee::from_staking_token_amount(*base_fee)); + state_ref.raw_accumulate_tip(Fee::from_staking_token_amount(*tip)); + continue; + } + let pair = TradingPair::new(*asset_id, *STAKING_TOKEN_ASSET_ID); + let bsod = bsods + .get(&pair) + .expect("bsod should be present for chain-submitted swap"); + + let (base_input, tip_input) = if pair.asset_1() == *asset_id { + ((*base_fee, 0u64.into()), (*tip, 0u64.into())) + } else { + ((0u64.into(), *base_fee), (0u64.into(), *tip)) + }; + + let base_output = bsod.pro_rata_outputs(base_input); + let tip_output = bsod.pro_rata_outputs(tip_input); + tracing::debug!( + ?asset_id, + ?base_input, + ?tip_input, + ?base_output, + ?tip_output, + "claiming chain-submitted swap for alt fee token" + ); + + // Obtain the base fee and tip amounts in the native token, discarding any unfilled amounts. + let (swapped_base, swapped_tip) = if pair.asset_1() == *asset_id { + // If `asset_id` is `R_1` we want to pull the other leg of the pair. + (base_output.1, tip_output.1) + } else { + // and vice-versa. `R_1` contains native tokens. + (base_output.0, tip_output.0) + }; + + // Finally, accumulate the swapped base fee and tip back into the fee component. + // (We already took all the fees out). + state_ref.raw_accumulate_base_fee(Fee::from_staking_token_amount(swapped_base)); + state_ref.raw_accumulate_tip(Fee::from_staking_token_amount(swapped_tip)); + } } // 3. Perform arbitrage to ensure all prices are consistent post-execution: diff --git a/crates/core/component/dex/src/component/flow.rs b/crates/core/component/dex/src/component/flow.rs index fc3a47bd3d..cda0a36abe 100644 --- a/crates/core/component/dex/src/component/flow.rs +++ b/crates/core/component/dex/src/component/flow.rs @@ -21,3 +21,9 @@ impl DerefMut for SwapFlow { &mut self.0 } } + +impl From<(Amount, Amount)> for SwapFlow { + fn from(tuple: (Amount, Amount)) -> Self { + Self(tuple) + } +} diff --git a/crates/core/component/dex/src/component/mod.rs b/crates/core/component/dex/src/component/mod.rs index 286b7717d6..454f1ca53d 100644 --- a/crates/core/component/dex/src/component/mod.rs +++ b/crates/core/component/dex/src/component/mod.rs @@ -26,6 +26,7 @@ pub use swap_manager::SwapDataRead; pub(crate) use arb::Arbitrage; pub(crate) use circuit_breaker::ExecutionCircuitBreaker; pub(crate) use circuit_breaker::ValueCircuitBreaker; +pub use circuit_breaker::ValueCircuitBreakerRead; pub(crate) use dex::InternalDexWrite; pub(crate) use swap_manager::SwapDataWrite; pub(crate) use swap_manager::SwapManager; diff --git a/crates/core/component/dex/src/component/router/route_and_fill.rs b/crates/core/component/dex/src/component/router/route_and_fill.rs index 3ab089fcf2..f72443de6c 100644 --- a/crates/core/component/dex/src/component/router/route_and_fill.rs +++ b/crates/core/component/dex/src/component/router/route_and_fill.rs @@ -33,7 +33,7 @@ pub trait HandleBatchSwaps: StateWrite + Sized { block_height: u64, params: RoutingParams, execution_budget: u32, - ) -> Result<()> + ) -> Result where Self: 'static, { @@ -127,7 +127,7 @@ pub trait HandleBatchSwaps: StateWrite + Sized { .set_output_data(output_data, swap_execution_1_for_2, swap_execution_2_for_1) .await?; - Ok(()) + Ok(output_data) } } diff --git a/crates/core/component/dex/src/component/router/tests.rs b/crates/core/component/dex/src/component/router/tests.rs index d10e95752b..f25018e4cb 100644 --- a/crates/core/component/dex/src/component/router/tests.rs +++ b/crates/core/component/dex/src/component/router/tests.rs @@ -1024,7 +1024,7 @@ async fn best_position_route_and_fill() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()) + .accumulate_swap_flow(&trading_pair, swap_flow.clone()) .await .unwrap(); let routing_params = state.routing_params().await.unwrap(); @@ -1165,7 +1165,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()) + .accumulate_swap_flow(&trading_pair, swap_flow.clone()) .await .unwrap(); let routing_params = state.routing_params().await.unwrap(); diff --git a/crates/core/component/dex/src/component/swap_manager.rs b/crates/core/component/dex/src/component/swap_manager.rs index f2f59f25bd..75b3c00766 100644 --- a/crates/core/component/dex/src/component/swap_manager.rs +++ b/crates/core/component/dex/src/component/swap_manager.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; use penumbra_asset::Value; @@ -49,8 +47,8 @@ pub trait SwapDataRead: StateRead { self.swap_flows().get(pair).cloned().unwrap_or_default() } - fn swap_flows(&self) -> BTreeMap { - self.object_get::>(state_key::swap_flows()) + fn swap_flows(&self) -> im::OrdMap { + self.object_get::>(state_key::swap_flows()) .unwrap_or_default() } @@ -63,15 +61,16 @@ pub trait SwapDataRead: StateRead { impl SwapDataRead for T {} pub(crate) trait SwapDataWrite: StateWrite { - async fn put_swap_flow( + async fn accumulate_swap_flow( &mut self, trading_pair: &TradingPair, swap_flow: SwapFlow, ) -> Result<()> { // Credit the DEX for the swap inflows. // - // Note that we credit the DEX for _all_ inflows, since we don't know - // how much will eventually be filled. + // At this point we don't know how much will eventually be filled, so we + // credit for all inflows, and then later debit for any unfilled input + // in the BSOD. self.dex_vcb_credit(Value { amount: swap_flow.0, asset_id: trading_pair.asset_1, @@ -83,10 +82,16 @@ pub(crate) trait SwapDataWrite: StateWrite { }) .await?; - // TODO: replace with IM struct later - let mut swap_flows = self.swap_flows(); - swap_flows.insert(*trading_pair, swap_flow); - self.object_put(state_key::swap_flows(), swap_flows); + // Accumulate the new swap flow into the map. + let old = self.swap_flows(); + let new = old.alter( + |maybe_flow| match maybe_flow { + Some(flow) => Some((flow.0 + swap_flow.0, flow.1 + swap_flow.1).into()), + None => Some(swap_flow), + }, + *trading_pair, + ); + self.object_put(state_key::swap_flows(), new); Ok(()) } diff --git a/crates/core/component/dex/src/component/tests.rs b/crates/core/component/dex/src/component/tests.rs index 6d43eedb99..318852c13e 100644 --- a/crates/core/component/dex/src/component/tests.rs +++ b/crates/core/component/dex/src/component/tests.rs @@ -628,7 +628,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()) + .accumulate_swap_flow(&trading_pair, swap_flow.clone()) .await .unwrap(); let routing_params = state.routing_params().await.unwrap(); @@ -736,7 +736,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()) + .accumulate_swap_flow(&trading_pair, swap_flow.clone()) .await .unwrap(); let routing_params = state.routing_params().await.unwrap(); diff --git a/crates/core/component/fee/Cargo.toml b/crates/core/component/fee/Cargo.toml index 4e3c5d7cc1..f1df6016e4 100644 --- a/crates/core/component/fee/Cargo.toml +++ b/crates/core/component/fee/Cargo.toml @@ -25,6 +25,7 @@ cnidarium = {workspace = true, optional = true, default-features = true} cnidarium-component = {workspace = true, optional = true, default-features = true} decaf377 = {workspace = true, default-features = true} decaf377-rdsa = {workspace = true} +im = {workspace = true} metrics = {workspace = true} penumbra-asset = {workspace = true, default-features = false} penumbra-num = {workspace = true, default-features = false} diff --git a/crates/core/component/fee/src/component.rs b/crates/core/component/fee/src/component.rs index b5dbb26071..e3c80d9f73 100644 --- a/crates/core/component/fee/src/component.rs +++ b/crates/core/component/fee/src/component.rs @@ -1,21 +1,26 @@ +mod fee_pay; pub mod rpc; mod view; use std::sync::Arc; -use crate::genesis; +use crate::{genesis, Fee}; use async_trait::async_trait; use cnidarium::StateWrite; use cnidarium_component::Component; +use penumbra_proto::core::component::fee::v1 as pb; +use penumbra_proto::state::StateWriteProto as _; use tendermint::abci; use tracing::instrument; + +pub use fee_pay::FeePay; pub use view::{StateReadExt, StateWriteExt}; // Fee component -pub struct Fee {} +pub struct FeeComponent {} #[async_trait] -impl Component for Fee { +impl Component for FeeComponent { type AppState = genesis::Content; #[instrument(name = "fee", skip(state, app_state))] @@ -35,12 +40,27 @@ impl Component for Fee { ) { } - #[instrument(name = "fee", skip(_state, _end_block))] + #[instrument(name = "fee", skip(state, _end_block))] async fn end_block( - _state: &mut Arc, + state: &mut Arc, _end_block: &abci::request::EndBlock, ) { - // TODO: update gas prices here eventually + let state_ref = Arc::get_mut(state).expect("unique ref in end_block"); + // Grab the total fees and use them to emit an event. + let fees = state_ref.accumulated_base_fees_and_tips(); + + let (swapped_base, swapped_tip) = fees + .get(&penumbra_asset::STAKING_TOKEN_ASSET_ID) + .cloned() + .unwrap_or_default(); + + let swapped_total = swapped_base + swapped_tip; + + state_ref.record_proto(pb::EventBlockFees { + swapped_fee_total: Some(Fee::from_staking_token_amount(swapped_total).into()), + swapped_base_fee_total: Some(Fee::from_staking_token_amount(swapped_base).into()), + swapped_tip_total: Some(Fee::from_staking_token_amount(swapped_tip).into()), + }); } #[instrument(name = "fee", skip(_state))] diff --git a/crates/core/component/fee/src/component/fee_pay.rs b/crates/core/component/fee/src/component/fee_pay.rs new file mode 100644 index 0000000000..c0272f5b4c --- /dev/null +++ b/crates/core/component/fee/src/component/fee_pay.rs @@ -0,0 +1,77 @@ +use anyhow::{ensure, Result}; +use async_trait::async_trait; +use cnidarium::StateWrite; +use penumbra_asset::Value; +use penumbra_proto::core::component::fee::v1 as pb; +use penumbra_proto::state::StateWriteProto as _; + +use crate::{Fee, Gas}; + +use super::view::{StateReadExt, StateWriteExt}; + +/// Allows payment of transaction fees. +#[async_trait] +pub trait FeePay: StateWrite { + /// Uses the provided `fee` to pay for `gas_used`, erroring if the fee is insufficient. + async fn pay_fee(&mut self, gas_used: Gas, fee: Fee) -> Result<()> { + let current_gas_prices = if fee.asset_id() == *penumbra_asset::STAKING_TOKEN_ASSET_ID { + self.get_gas_prices() + .await + .expect("gas prices must be present in state") + } else { + let alt_gas_prices = self + .get_alt_gas_prices() + .await + .expect("alt gas prices must be present in state"); + // This does a linear scan, but we think that's OK because we're expecting + // a small number of alt gas prices before switching to the DEX directly. + alt_gas_prices + .into_iter() + .find(|prices| prices.asset_id == fee.asset_id()) + .ok_or_else(|| { + anyhow::anyhow!("fee token {} not recognized by the chain", fee.asset_id()) + })? + }; + + // Double check that the gas price assets match. + ensure!( + current_gas_prices.asset_id == fee.asset_id(), + "unexpected mismatch between fee and queried gas prices (expected: {}, found: {})", + fee.asset_id(), + current_gas_prices.asset_id, + ); + + // Compute the base fee for the `gas_used`. + let base_fee = current_gas_prices.fee(&gas_used); + + // The provided fee must be at least the base fee. + ensure!( + fee.amount() >= base_fee.amount(), + "fee must be greater than or equal to the transaction base price (supplied: {}, base: {})", + fee.amount(), + base_fee.amount(), + ); + + // Otherwise, the fee less the base fee is the proposer tip. + let tip = Fee(Value { + amount: fee.amount() - base_fee.amount(), + asset_id: fee.asset_id(), + }); + + // Record information about the fee payment in an event. + self.record_proto(pb::EventPaidFee { + fee: Some(fee.into()), + base_fee: Some(base_fee.into()), + gas_used: Some(gas_used.into()), + tip: Some(tip.into()), + }); + + // Finally, queue the paid fee for processing at the end of the block. + self.raw_accumulate_base_fee(base_fee); + self.raw_accumulate_tip(tip); + + Ok(()) + } +} + +impl FeePay for S {} diff --git a/crates/core/component/fee/src/component/view.rs b/crates/core/component/fee/src/component/view.rs index 89d2aef8b2..ab95a02a41 100644 --- a/crates/core/component/fee/src/component/view.rs +++ b/crates/core/component/fee/src/component/view.rs @@ -1,9 +1,11 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; +use penumbra_asset::asset; +use penumbra_num::Amount; use penumbra_proto::{StateReadProto, StateWriteProto}; -use crate::{params::FeeParameters, state_key, GasPrices}; +use crate::{params::FeeParameters, state_key, Fee, GasPrices}; /// This trait provides read access to fee-related parts of the Penumbra /// state store. @@ -41,12 +43,16 @@ pub trait StateReadExt: StateRead { self.object_get::<()>(state_key::gas_prices_changed()) .is_some() } + + /// The accumulated base fees and tips for this block, indexed by asset ID. + fn accumulated_base_fees_and_tips(&self) -> im::OrdMap { + self.object_get(state_key::fee_accumulator()) + .unwrap_or_default() + } } impl StateReadExt for T {} -/// This trait provides write access to common parts of the Penumbra -/// state store. #[async_trait] pub trait StateWriteExt: StateWrite { /// Writes the provided fee parameters to the JMT. @@ -67,6 +73,38 @@ pub trait StateWriteExt: StateWrite { self.object_put(state_key::gas_prices_changed(), ()); } */ + + /// Takes the accumulated base fees and tips for this block, resetting them to zero. + fn take_accumulated_base_fees_and_tips(&mut self) -> im::OrdMap { + let old = self.accumulated_base_fees_and_tips(); + let new = im::OrdMap::::new(); + self.object_put(state_key::fee_accumulator(), new); + old + } + + fn raw_accumulate_base_fee(&mut self, base_fee: Fee) { + let old = self.accumulated_base_fees_and_tips(); + let new = old.alter( + |maybe_amounts| match maybe_amounts { + Some((base, tip)) => Some((base + base_fee.amount(), tip)), + None => Some((base_fee.amount(), Amount::zero())), + }, + base_fee.asset_id(), + ); + self.object_put(state_key::fee_accumulator(), new); + } + + fn raw_accumulate_tip(&mut self, tip_fee: Fee) { + let old = self.accumulated_base_fees_and_tips(); + let new = old.alter( + |maybe_amounts| match maybe_amounts { + Some((base, tip)) => Some((base, tip + tip_fee.amount())), + None => Some((Amount::zero(), tip_fee.amount())), + }, + tip_fee.asset_id(), + ); + self.object_put(state_key::fee_accumulator(), new); + } } impl StateWriteExt for T {} diff --git a/crates/core/component/fee/src/gas.rs b/crates/core/component/fee/src/gas.rs index c4a933fb88..c06bbc170a 100644 --- a/crates/core/component/fee/src/gas.rs +++ b/crates/core/component/fee/src/gas.rs @@ -14,7 +14,8 @@ use crate::Fee; /// Represents the different resources that a transaction can consume, /// for purposes of calculating multidimensional fees based on real /// transaction resource consumption. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "pb::Gas", into = "pb::Gas")] pub struct Gas { pub block_space: u64, pub compact_block_space: u64, @@ -22,6 +23,34 @@ pub struct Gas { pub execution: u64, } +impl DomainType for Gas { + type Proto = pb::Gas; +} + +impl From for pb::Gas { + fn from(gas: Gas) -> Self { + pb::Gas { + block_space: gas.block_space, + compact_block_space: gas.compact_block_space, + verification: gas.verification, + execution: gas.execution, + } + } +} + +impl TryFrom for Gas { + type Error = anyhow::Error; + + fn try_from(proto: pb::Gas) -> Result { + Ok(Gas { + block_space: proto.block_space, + compact_block_space: proto.compact_block_space, + verification: proto.verification, + execution: proto.execution, + }) + } +} + impl Gas { pub fn zero() -> Self { Self { diff --git a/crates/core/component/fee/src/state_key.rs b/crates/core/component/fee/src/state_key.rs index 9e2c72911a..7ea6575557 100644 --- a/crates/core/component/fee/src/state_key.rs +++ b/crates/core/component/fee/src/state_key.rs @@ -9,3 +9,7 @@ pub fn gas_prices() -> &'static str { pub fn gas_prices_changed() -> &'static str { "fee/gas_prices_changed" } + +pub fn fee_accumulator() -> &'static str { + "fee/accumulator" +} 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 9af646bae8..e3ae15a7b0 100644 --- a/crates/proto/src/gen/penumbra.core.component.fee.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.fee.v1.rs @@ -17,6 +17,32 @@ impl ::prost::Name for Fee { ::prost::alloc::format!("penumbra.core.component.fee.v1.{}", Self::NAME) } } +/// Gas usage for a transaction. +/// +/// Gas used is multiplied by `GasPrices` to determine a `Fee`. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Gas { + /// The amount of block space used. + #[prost(uint64, tag = "1")] + pub block_space: u64, + /// The amount of compact block space used. + #[prost(uint64, tag = "2")] + pub compact_block_space: u64, + /// The amount of verification cost used. + #[prost(uint64, tag = "3")] + pub verification: u64, + /// The amount of execution cost used. + #[prost(uint64, tag = "4")] + pub execution: u64, +} +impl ::prost::Name for Gas { + const NAME: &'static str = "Gas"; + const PACKAGE: &'static str = "penumbra.core.component.fee.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.component.fee.v1.{}", Self::NAME) + } +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GasPrices { @@ -176,6 +202,51 @@ impl ::prost::Name for CurrentGasPricesResponse { ::prost::alloc::format!("penumbra.core.component.fee.v1.{}", Self::NAME) } } +/// Emitted during fee payment. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventPaidFee { + /// The fee paid. + #[prost(message, optional, tag = "1")] + pub fee: ::core::option::Option, + /// The base fee that was required. + #[prost(message, optional, tag = "2")] + pub base_fee: ::core::option::Option, + /// The tip that was paid to the proposer. + #[prost(message, optional, tag = "3")] + pub tip: ::core::option::Option, + /// The gas used to compute the base fee. + #[prost(message, optional, tag = "4")] + pub gas_used: ::core::option::Option, +} +impl ::prost::Name for EventPaidFee { + const NAME: &'static str = "EventPaidFee"; + const PACKAGE: &'static str = "penumbra.core.component.fee.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.component.fee.v1.{}", Self::NAME) + } +} +/// Emitted as a summary of fees in the block. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventBlockFees { + /// The total fees, after swapping to the native token. + #[prost(message, optional, tag = "1")] + pub swapped_fee_total: ::core::option::Option, + /// The total base fees, after swapping to the native token. + #[prost(message, optional, tag = "2")] + pub swapped_base_fee_total: ::core::option::Option, + /// The total tips, after swapping to the native token. + #[prost(message, optional, tag = "3")] + pub swapped_tip_total: ::core::option::Option, +} +impl ::prost::Name for EventBlockFees { + const NAME: &'static str = "EventBlockFees"; + const PACKAGE: &'static str = "penumbra.core.component.fee.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.component.fee.v1.{}", Self::NAME) + } +} /// Generated client implementations. #[cfg(feature = "rpc")] pub mod query_service_client { 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 feabfe80a2..2d9df3b9be 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 @@ -184,6 +184,286 @@ impl<'de> serde::Deserialize<'de> for CurrentGasPricesResponse { deserializer.deserialize_struct("penumbra.core.component.fee.v1.CurrentGasPricesResponse", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for EventBlockFees { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.swapped_fee_total.is_some() { + len += 1; + } + if self.swapped_base_fee_total.is_some() { + len += 1; + } + if self.swapped_tip_total.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.fee.v1.EventBlockFees", len)?; + if let Some(v) = self.swapped_fee_total.as_ref() { + struct_ser.serialize_field("swappedFeeTotal", v)?; + } + if let Some(v) = self.swapped_base_fee_total.as_ref() { + struct_ser.serialize_field("swappedBaseFeeTotal", v)?; + } + if let Some(v) = self.swapped_tip_total.as_ref() { + struct_ser.serialize_field("swappedTipTotal", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventBlockFees { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "swapped_fee_total", + "swappedFeeTotal", + "swapped_base_fee_total", + "swappedBaseFeeTotal", + "swapped_tip_total", + "swappedTipTotal", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + SwappedFeeTotal, + SwappedBaseFeeTotal, + SwappedTipTotal, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "swappedFeeTotal" | "swapped_fee_total" => Ok(GeneratedField::SwappedFeeTotal), + "swappedBaseFeeTotal" | "swapped_base_fee_total" => Ok(GeneratedField::SwappedBaseFeeTotal), + "swappedTipTotal" | "swapped_tip_total" => Ok(GeneratedField::SwappedTipTotal), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventBlockFees; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.fee.v1.EventBlockFees") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut swapped_fee_total__ = None; + let mut swapped_base_fee_total__ = None; + let mut swapped_tip_total__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::SwappedFeeTotal => { + if swapped_fee_total__.is_some() { + return Err(serde::de::Error::duplicate_field("swappedFeeTotal")); + } + swapped_fee_total__ = map_.next_value()?; + } + GeneratedField::SwappedBaseFeeTotal => { + if swapped_base_fee_total__.is_some() { + return Err(serde::de::Error::duplicate_field("swappedBaseFeeTotal")); + } + swapped_base_fee_total__ = map_.next_value()?; + } + GeneratedField::SwappedTipTotal => { + if swapped_tip_total__.is_some() { + return Err(serde::de::Error::duplicate_field("swappedTipTotal")); + } + swapped_tip_total__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventBlockFees { + swapped_fee_total: swapped_fee_total__, + swapped_base_fee_total: swapped_base_fee_total__, + swapped_tip_total: swapped_tip_total__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.fee.v1.EventBlockFees", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for EventPaidFee { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.fee.is_some() { + len += 1; + } + if self.base_fee.is_some() { + len += 1; + } + if self.tip.is_some() { + len += 1; + } + if self.gas_used.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.fee.v1.EventPaidFee", len)?; + if let Some(v) = self.fee.as_ref() { + struct_ser.serialize_field("fee", v)?; + } + if let Some(v) = self.base_fee.as_ref() { + struct_ser.serialize_field("baseFee", v)?; + } + if let Some(v) = self.tip.as_ref() { + struct_ser.serialize_field("tip", v)?; + } + if let Some(v) = self.gas_used.as_ref() { + struct_ser.serialize_field("gasUsed", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventPaidFee { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "fee", + "base_fee", + "baseFee", + "tip", + "gas_used", + "gasUsed", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Fee, + BaseFee, + Tip, + GasUsed, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "fee" => Ok(GeneratedField::Fee), + "baseFee" | "base_fee" => Ok(GeneratedField::BaseFee), + "tip" => Ok(GeneratedField::Tip), + "gasUsed" | "gas_used" => Ok(GeneratedField::GasUsed), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventPaidFee; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.fee.v1.EventPaidFee") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut fee__ = None; + let mut base_fee__ = None; + let mut tip__ = None; + let mut gas_used__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Fee => { + if fee__.is_some() { + return Err(serde::de::Error::duplicate_field("fee")); + } + fee__ = map_.next_value()?; + } + GeneratedField::BaseFee => { + if base_fee__.is_some() { + return Err(serde::de::Error::duplicate_field("baseFee")); + } + base_fee__ = map_.next_value()?; + } + GeneratedField::Tip => { + if tip__.is_some() { + return Err(serde::de::Error::duplicate_field("tip")); + } + tip__ = map_.next_value()?; + } + GeneratedField::GasUsed => { + if gas_used__.is_some() { + return Err(serde::de::Error::duplicate_field("gasUsed")); + } + gas_used__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventPaidFee { + fee: fee__, + base_fee: base_fee__, + tip: tip__, + gas_used: gas_used__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.fee.v1.EventPaidFee", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for Fee { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -586,6 +866,166 @@ impl<'de> serde::Deserialize<'de> for fee_tier::Tier { deserializer.deserialize_any(GeneratedVisitor) } } +impl serde::Serialize for Gas { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.block_space != 0 { + len += 1; + } + if self.compact_block_space != 0 { + len += 1; + } + if self.verification != 0 { + len += 1; + } + if self.execution != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.fee.v1.Gas", len)?; + if self.block_space != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("blockSpace", ToString::to_string(&self.block_space).as_str())?; + } + if self.compact_block_space != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("compactBlockSpace", ToString::to_string(&self.compact_block_space).as_str())?; + } + if self.verification != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("verification", ToString::to_string(&self.verification).as_str())?; + } + if self.execution != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("execution", ToString::to_string(&self.execution).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Gas { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "block_space", + "blockSpace", + "compact_block_space", + "compactBlockSpace", + "verification", + "execution", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + BlockSpace, + CompactBlockSpace, + Verification, + Execution, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "blockSpace" | "block_space" => Ok(GeneratedField::BlockSpace), + "compactBlockSpace" | "compact_block_space" => Ok(GeneratedField::CompactBlockSpace), + "verification" => Ok(GeneratedField::Verification), + "execution" => Ok(GeneratedField::Execution), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Gas; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.fee.v1.Gas") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut block_space__ = None; + let mut compact_block_space__ = None; + let mut verification__ = None; + let mut execution__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::BlockSpace => { + if block_space__.is_some() { + return Err(serde::de::Error::duplicate_field("blockSpace")); + } + block_space__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::CompactBlockSpace => { + if compact_block_space__.is_some() { + return Err(serde::de::Error::duplicate_field("compactBlockSpace")); + } + compact_block_space__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Verification => { + if verification__.is_some() { + return Err(serde::de::Error::duplicate_field("verification")); + } + verification__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Execution => { + if execution__.is_some() { + return Err(serde::de::Error::duplicate_field("execution")); + } + execution__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(Gas { + block_space: block_space__.unwrap_or_default(), + compact_block_space: compact_block_space__.unwrap_or_default(), + verification: verification__.unwrap_or_default(), + execution: execution__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.fee.v1.Gas", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for GasPrices { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 7666f69fd856cc6d3184f646884cd0e34189e2cb..da6e9112163e8dd5cc38597223c5e97d5dea53c7 100644 GIT binary patch delta 2842 zcmb7G%TF6e828SOUjqqvc^Ly?AcO$nVIY(=k19Y2RX`#TBBGRn7keE{{Hk|tl9Llv zRW7|$dvMjNQf@s}s+3Aq4^=BwmD)=VRh1~0sHdoZL)AlnGrL|Ww;WcQZ@%yM`<^qi zZyxh+KI7kBxvc&fXEo&ywX^oq7HzgOmtR*uSD3n_n_4ex*v%G__jk;qo;0EgSBB_1 zlUujMjyBF(l7$?W*3MBwA`K(1YF;A)cwi*U z(t=8h!;NRxuRK+w{r@Vgaj|0Lt($r#b=@$u8K$NUqjvL4e}`SX8nHik6%M_}Lc6+Y z?AY;F6HP&}bSsb@!gNqa z93fQK5h5RLt~=Ps6s4*C=IR&Wzc>qM)aN^>tvy#-pM5o#RH}Q7P(`DfuSU#Tv(W8QDWGcafHdJK3bDK7R*X)cK> zaBb}s<+5W{6Kq^2(9!LdfC|nOuNRl3N=NWJbNMxv=+}E`Ak#IQ;R&N z>4`%b=F`m)Cod8Dti8I_7guHDXGuVo1rlJh!KS(d(pmVJz!Xu9Uh=KjPnXWMh)P}x zwXq)+6p0G5Is5g}50hA8isndzghPVeT%&V7Ac1TS=i?JqqM%!T_S^f9mG>x7(6nXZ zoL5L7S#EGnC?sLnHtbT0sahT#W9JZ&U!$wOb^FPUvzi}9tD$yz_WUBCHT&lq@i>;? zS|d(AkYKjf>`W*maIIlNXBY{f(YJkfY`#3xemm64Mi>pq25*vEu^zr;ShYvL`}v20 zU^CDzf)RSubhBf;E`e_I-8B|QXz+U1*Dp_80Fb+(UO8w%jc)mLXV8M!-wJWIglYsu z$lLbc%L`Lj0=(_k5E9I`8>L5K1|QpPP)`0wY`XjzKR&pqkY6UBrj0BJ=|k0;*}TA zj)mBZr9!b_8YTD901O`XJr5$leJ^%#OYI|eanpx{kR{a?_o~MOFRye#EZ&W>Wk?70 zq&x>I9ArHQ;<#qL48)C=MFtnpoUlfVz5^vT>NN)*bZ(Y&Ilc5yJbb9TS>>6$<461# zxy=fLw1`tNk2u4kStZgPn{f&u{vRZAlV3Lg4hf94e^SL`2q)m zF1bNN0)HvcA#_!tMAW5Nmz?OZsF;Pf^x_eB^%|IOCPq_MlQh4?T#2(n9s7 zMZ`l8kvS{~Mf?DQP(c*Dc<>tto_gp(@f!$Ud}fnPJq*kzgjNY*GJswZ#w9991=R%Tv{c0At)y%k7#2= z2*EfZCW{y`@lbG1sAjUb{9N0a#Zc;qqEWGOmn-<7wQ6Earcl*yW9=X)pxnkr6+T@Qt@<$irzZUga8WNJ0z zh#e2%CQHjpjr$Ekc>r^2^qf!{OAn-cg-z+vH0q?Bn%vQ*x;0Z!ju8<>m{G2oJVz(w zpN8`o0-;Wg7Bqpum;G zX}16==IuhPa&T*Pk1Q}saCsFfdLV&GC6w@$0+S_Q!K5-hBTgbSZt_KKNjl9* zQ{C~RVTp6F<|?78yw;p8V+yY-?bZbatQzgUMkshy@fyzys^h|IT7%dn>HM(e_hxFS zW+cRv;JSC;b@{hBeZ8(*Dh;)SuGWW7`3gq$3+rM4N#W_OX!ue1Mgg3*VmRJY(@nmn zUF|+K9qVgGR@9KJu99uZC#~vPOp$D>*F7j;t+02Z!f-}w#1