diff --git a/Cargo.lock b/Cargo.lock index 7fb1186a9b..9b067631f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5166,6 +5166,7 @@ dependencies = [ "penumbra-sdk-sct", "serde", "tendermint 0.40.1", + "tonic", "tracing", ] diff --git a/crates/core/component/distributions/Cargo.toml b/crates/core/component/distributions/Cargo.toml index f6c31d9357..cd569b1212 100644 --- a/crates/core/component/distributions/Cargo.toml +++ b/crates/core/component/distributions/Cargo.toml @@ -13,6 +13,7 @@ component = [ "cnidarium", "penumbra-sdk-proto/cnidarium", "penumbra-sdk-sct/component", + "tonic", ] default = ["component"] docsrs = [] @@ -29,6 +30,7 @@ penumbra-sdk-sct = {workspace = true, default-features = false} serde = {workspace = true, features = ["derive"]} tendermint = {workspace = true} tracing = {workspace = true} +tonic = {workspace = true, optional = true} [dev-dependencies] getrandom = {workspace = true} diff --git a/crates/core/component/distributions/src/component.rs b/crates/core/component/distributions/src/component.rs index 568de1e362..5891110e25 100644 --- a/crates/core/component/distributions/src/component.rs +++ b/crates/core/component/distributions/src/component.rs @@ -1,5 +1,6 @@ pub mod state_key; pub use view::{StateReadExt, StateWriteExt}; +pub mod rpc; mod view; @@ -10,6 +11,7 @@ use async_trait::async_trait; use cnidarium::StateWrite; use cnidarium_component::Component; use penumbra_sdk_num::Amount; +use penumbra_sdk_sct::{component::clock::EpochRead, epoch::Epoch}; use tendermint::v0_37::abci; use tracing::instrument; @@ -99,16 +101,16 @@ trait DistributionManager: StateWriteExt { /// Update the object store with the new issuance of staking tokens for this epoch. async fn define_staking_budget(&mut self) -> Result<()> { let new_issuance = self.compute_new_staking_issuance().await?; - tracing::debug!(?new_issuance, "computed new staking issuance for epoch"); + tracing::debug!( + ?new_issuance, + "computed new staking issuance for current epoch" + ); Ok(self.set_staking_token_issuance_for_epoch(new_issuance)) } /// Computes total LQT reward issuance for the epoch. - async fn compute_new_lqt_issuance(&self) -> Result { - use penumbra_sdk_sct::component::clock::EpochRead; - + async fn compute_new_lqt_issuance(&self, current_epoch: Epoch) -> Result { let current_block_height = self.get_block_height().await?; - let current_epoch = self.get_current_epoch().await?; let epoch_length = current_block_height .checked_sub(current_epoch.start_height) .unwrap_or_else(|| panic!("epoch start height is greater than current block height (epoch_start={}, current_height={}", current_epoch.start_height, current_block_height)); @@ -138,9 +140,16 @@ trait DistributionManager: StateWriteExt { /// Update the nonverifiable storage with the newly issued LQT rewards for the current epoch. async fn define_lqt_budget(&mut self) -> Result<()> { - let new_issuance = self.compute_new_lqt_issuance().await?; - tracing::debug!(?new_issuance, "computed new lqt reward issuance for epoch"); - Ok(self.set_lqt_reward_issuance_for_epoch(new_issuance)) + // Grab the ambient epoch index. + let current_epoch = self.get_current_epoch().await?; + + let new_issuance = self.compute_new_lqt_issuance(current_epoch).await?; + tracing::debug!( + ?new_issuance, + "computed new lqt reward issuance for epoch {}", + current_epoch.index + ); + Ok(self.set_lqt_reward_issuance_for_epoch(current_epoch.index, new_issuance)) } } diff --git a/crates/core/component/distributions/src/component/rpc.rs b/crates/core/component/distributions/src/component/rpc.rs new file mode 100644 index 0000000000..82b3f2bc69 --- /dev/null +++ b/crates/core/component/distributions/src/component/rpc.rs @@ -0,0 +1,80 @@ +use cnidarium::Storage; +use penumbra_sdk_num::Amount; +use penumbra_sdk_proto::core::component::distributions::v1::{ + self as pb, distributions_service_server::DistributionsService, +}; +use penumbra_sdk_sct::component::clock::EpochRead; + +use crate::component::StateReadExt; + +pub struct Server { + storage: Storage, +} + +impl Server { + pub fn new(storage: Storage) -> Self { + Self { storage } + } +} + +#[tonic::async_trait] +impl DistributionsService for Server { + async fn current_lqt_pool_size( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + // Retrieve latest state snapshot. + let state = self.storage.latest_snapshot(); + + let current_block_height = state.get_block_height().await.map_err(|e| { + tonic::Status::internal(format!("failed to get current block height: {}", e)) + })?; + let current_epoch = state + .get_current_epoch() + .await + .map_err(|e| tonic::Status::internal(format!("failed to get current epoch: {}", e)))?; + let epoch_length = current_block_height + .checked_sub(current_epoch.start_height) + .unwrap_or_else(|| panic!("epoch start height is greater than current block height (epoch_start={}, current_height={}", current_epoch.start_height, current_block_height)); + + let lqt_block_reward_rate = state + .get_distributions_params() + .await + .map_err(|e| { + tonic::Status::internal(format!("failed to get distributions parameters: {}", e)) + })? + .liquidity_tournament_incentive_per_block as u64; + + let current_lqt_pool_size = lqt_block_reward_rate + .checked_mul(epoch_length as u64) + .expect("infallible unless issuance is pathological"); + + Ok(tonic::Response::new(pb::CurrentLqtPoolSizeResponse { + epoch_index: current_epoch.index, + pool_size: Some(Amount::from(current_lqt_pool_size).into()), + })) + } + + async fn lqt_pool_size_by_epoch( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + // Retrieve latest state snapshot. + let state = self.storage.latest_snapshot(); + let epoch_index = request.into_inner().epoch; + let amount = state + .get_lqt_reward_issuance_for_epoch(epoch_index) + .await + .ok_or_else(|| { + tonic::Status::not_found(format!( + "failed to retrieve LQT issuance for epoch {} from non-verifiable storage", + epoch_index, + )) + })?; + + Ok(tonic::Response::new(pb::LqtPoolSizeByEpochResponse { + epoch_index, + pool_size: Some(amount.into()), + })) + } +} diff --git a/crates/core/component/distributions/src/component/state_key.rs b/crates/core/component/distributions/src/component/state_key.rs index f82bf0b18f..a42cc7255c 100644 --- a/crates/core/component/distributions/src/component/state_key.rs +++ b/crates/core/component/distributions/src/component/state_key.rs @@ -3,9 +3,22 @@ pub fn staking_token_issuance_for_epoch() -> &'static str { "distributions/staking_token_issuance_for_epoch" } -// The amount of LQT rewards issued for this epoch. -pub fn lqt_reward_issuance_for_epoch() -> &'static str { - "distributions/lqt_reward_issuance_for_epoch" +pub mod lqt { + pub mod v1 { + pub mod budget { + pub(crate) fn prefix(epoch_index: u64) -> String { + format!("distributions/lqt/v1/budget/{epoch_index:020}") + } + + /// The amount of LQT rewards issued for this epoch. + pub fn for_epoch(epoch_index: u64) -> [u8; 48] { + let prefix_bytes = prefix(epoch_index); + let mut key = [0u8; 48]; + key[0..48].copy_from_slice(prefix_bytes.as_bytes()); + key + } + } + } } pub fn distributions_parameters() -> &'static str { diff --git a/crates/core/component/distributions/src/component/view.rs b/crates/core/component/distributions/src/component/view.rs index 9e1b923ae9..facddf9d0c 100644 --- a/crates/core/component/distributions/src/component/view.rs +++ b/crates/core/component/distributions/src/component/view.rs @@ -21,8 +21,13 @@ pub trait StateReadExt: StateRead { } // Get the total amount of LQT rewards issued for this epoch. - fn get_lqt_reward_issuance_for_epoch(&self) -> Option { - self.object_get(&state_key::lqt_reward_issuance_for_epoch()) + async fn get_lqt_reward_issuance_for_epoch(&self, epoch_index: u64) -> Option { + let key = state_key::lqt::v1::budget::for_epoch(epoch_index); + + self.nonverifiable_get(&key).await.unwrap_or_else(|_| { + tracing::error!("LQT issuance does not exist for epoch"); + None + }) } } @@ -41,8 +46,11 @@ pub trait StateWriteExt: StateWrite + StateReadExt { } /// Set the total amount of LQT rewards issued for this epoch. - fn set_lqt_reward_issuance_for_epoch(&mut self, issuance: Amount) { - self.object_put(state_key::lqt_reward_issuance_for_epoch(), issuance); + fn set_lqt_reward_issuance_for_epoch(&mut self, epoch_index: u64, issuance: Amount) { + self.nonverifiable_put( + state_key::lqt::v1::budget::for_epoch(epoch_index).into(), + issuance, + ); } } impl StateWriteExt for T {} diff --git a/crates/proto/src/gen/penumbra.core.component.distributions.v1.rs b/crates/proto/src/gen/penumbra.core.component.distributions.v1.rs index 5c819c4835..f2eaca6843 100644 --- a/crates/proto/src/gen/penumbra.core.component.distributions.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.distributions.v1.rs @@ -35,3 +35,469 @@ impl ::prost::Name for GenesisContent { "/penumbra.core.component.distributions.v1.GenesisContent".into() } } +/// Request for retrieving the pool size of the current epoch from the chain state. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct CurrentLqtPoolSizeRequest {} +impl ::prost::Name for CurrentLqtPoolSizeRequest { + const NAME: &'static str = "CurrentLqtPoolSizeRequest"; + const PACKAGE: &'static str = "penumbra.core.component.distributions.v1"; + fn full_name() -> ::prost::alloc::string::String { + "penumbra.core.component.distributions.v1.CurrentLqtPoolSizeRequest".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/penumbra.core.component.distributions.v1.CurrentLqtPoolSizeRequest".into() + } +} +/// Response containing the pool size for the current epoch. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct CurrentLqtPoolSizeResponse { + /// The current epoch index. + #[prost(uint64, tag = "1")] + pub epoch_index: u64, + /// The total LQT pool size for the current epoch. + #[prost(message, optional, tag = "2")] + pub pool_size: ::core::option::Option, +} +impl ::prost::Name for CurrentLqtPoolSizeResponse { + const NAME: &'static str = "CurrentLqtPoolSizeResponse"; + const PACKAGE: &'static str = "penumbra.core.component.distributions.v1"; + fn full_name() -> ::prost::alloc::string::String { + "penumbra.core.component.distributions.v1.CurrentLqtPoolSizeResponse".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/penumbra.core.component.distributions.v1.CurrentLqtPoolSizeResponse".into() + } +} +/// Request for retrieving the pool size at a specific epoch. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct LqtPoolSizeByEpochRequest { + /// The epoch for which we want to retrieve the pool size. + #[prost(uint64, tag = "1")] + pub epoch: u64, +} +impl ::prost::Name for LqtPoolSizeByEpochRequest { + const NAME: &'static str = "LqtPoolSizeByEpochRequest"; + const PACKAGE: &'static str = "penumbra.core.component.distributions.v1"; + fn full_name() -> ::prost::alloc::string::String { + "penumbra.core.component.distributions.v1.LqtPoolSizeByEpochRequest".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/penumbra.core.component.distributions.v1.LqtPoolSizeByEpochRequest".into() + } +} +/// Response containing the pool size at a specific epoch. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct LqtPoolSizeByEpochResponse { + /// The epoch for which the pool size is returned. + #[prost(uint64, tag = "1")] + pub epoch_index: u64, + /// The total LQT pool size for the given epoch. + #[prost(message, optional, tag = "2")] + pub pool_size: ::core::option::Option, +} +impl ::prost::Name for LqtPoolSizeByEpochResponse { + const NAME: &'static str = "LqtPoolSizeByEpochResponse"; + const PACKAGE: &'static str = "penumbra.core.component.distributions.v1"; + fn full_name() -> ::prost::alloc::string::String { + "penumbra.core.component.distributions.v1.LqtPoolSizeByEpochResponse".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/penumbra.core.component.distributions.v1.LqtPoolSizeByEpochResponse".into() + } +} +/// Generated client implementations. +#[cfg(feature = "rpc")] +pub mod distributions_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Query operations for the distributions component. + #[derive(Debug, Clone)] + pub struct DistributionsServiceClient { + inner: tonic::client::Grpc, + } + impl DistributionsServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl DistributionsServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> DistributionsServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + DistributionsServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn current_lqt_pool_size( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/penumbra.core.component.distributions.v1.DistributionsService/CurrentLqtPoolSize", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "penumbra.core.component.distributions.v1.DistributionsService", + "CurrentLqtPoolSize", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn lqt_pool_size_by_epoch( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/penumbra.core.component.distributions.v1.DistributionsService/LqtPoolSizeByEpoch", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "penumbra.core.component.distributions.v1.DistributionsService", + "LqtPoolSizeByEpoch", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "rpc")] +pub mod distributions_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with DistributionsServiceServer. + #[async_trait] + pub trait DistributionsService: std::marker::Send + std::marker::Sync + 'static { + async fn current_lqt_pool_size( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn lqt_pool_size_by_epoch( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Query operations for the distributions component. + #[derive(Debug)] + pub struct DistributionsServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl DistributionsServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> + for DistributionsServiceServer + where + T: DistributionsService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/penumbra.core.component.distributions.v1.DistributionsService/CurrentLqtPoolSize" => { + #[allow(non_camel_case_types)] + struct CurrentLqtPoolSizeSvc(pub Arc); + impl< + T: DistributionsService, + > tonic::server::UnaryService + for CurrentLqtPoolSizeSvc { + type Response = super::CurrentLqtPoolSizeResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::current_lqt_pool_size( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = CurrentLqtPoolSizeSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/penumbra.core.component.distributions.v1.DistributionsService/LqtPoolSizeByEpoch" => { + #[allow(non_camel_case_types)] + struct LqtPoolSizeByEpochSvc(pub Arc); + impl< + T: DistributionsService, + > tonic::server::UnaryService + for LqtPoolSizeByEpochSvc { + type Response = super::LqtPoolSizeByEpochResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::lqt_pool_size_by_epoch( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = LqtPoolSizeByEpochSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new(empty_body()); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for DistributionsServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "penumbra.core.component.distributions.v1.DistributionsService"; + impl tonic::server::NamedService for DistributionsServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/crates/proto/src/gen/penumbra.core.component.distributions.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.distributions.v1.serde.rs index 228b37042a..79b131f9a1 100644 --- a/crates/proto/src/gen/penumbra.core.component.distributions.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.distributions.v1.serde.rs @@ -1,3 +1,193 @@ +impl serde::Serialize for CurrentLqtPoolSizeRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("penumbra.core.component.distributions.v1.CurrentLqtPoolSizeRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for CurrentLqtPoolSizeRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + __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, + { + Ok(GeneratedField::__SkipField__) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = CurrentLqtPoolSizeRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.distributions.v1.CurrentLqtPoolSizeRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(CurrentLqtPoolSizeRequest { + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.distributions.v1.CurrentLqtPoolSizeRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for CurrentLqtPoolSizeResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.epoch_index != 0 { + len += 1; + } + if self.pool_size.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.distributions.v1.CurrentLqtPoolSizeResponse", len)?; + if self.epoch_index != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("epochIndex", ToString::to_string(&self.epoch_index).as_str())?; + } + if let Some(v) = self.pool_size.as_ref() { + struct_ser.serialize_field("poolSize", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for CurrentLqtPoolSizeResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "epoch_index", + "epochIndex", + "pool_size", + "poolSize", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + EpochIndex, + PoolSize, + __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 { + "epochIndex" | "epoch_index" => Ok(GeneratedField::EpochIndex), + "poolSize" | "pool_size" => Ok(GeneratedField::PoolSize), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = CurrentLqtPoolSizeResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.distributions.v1.CurrentLqtPoolSizeResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut epoch_index__ = None; + let mut pool_size__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::EpochIndex => { + if epoch_index__.is_some() { + return Err(serde::de::Error::duplicate_field("epochIndex")); + } + epoch_index__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::PoolSize => { + if pool_size__.is_some() { + return Err(serde::de::Error::duplicate_field("poolSize")); + } + pool_size__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(CurrentLqtPoolSizeResponse { + epoch_index: epoch_index__.unwrap_or_default(), + pool_size: pool_size__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.distributions.v1.CurrentLqtPoolSizeResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DistributionsParameters { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -216,3 +406,220 @@ impl<'de> serde::Deserialize<'de> for GenesisContent { deserializer.deserialize_struct("penumbra.core.component.distributions.v1.GenesisContent", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for LqtPoolSizeByEpochRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.epoch != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.distributions.v1.LqtPoolSizeByEpochRequest", len)?; + if self.epoch != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("epoch", ToString::to_string(&self.epoch).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for LqtPoolSizeByEpochRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "epoch", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Epoch, + __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 { + "epoch" => Ok(GeneratedField::Epoch), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = LqtPoolSizeByEpochRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.distributions.v1.LqtPoolSizeByEpochRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut epoch__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Epoch => { + if epoch__.is_some() { + return Err(serde::de::Error::duplicate_field("epoch")); + } + epoch__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(LqtPoolSizeByEpochRequest { + epoch: epoch__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.distributions.v1.LqtPoolSizeByEpochRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for LqtPoolSizeByEpochResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.epoch_index != 0 { + len += 1; + } + if self.pool_size.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.distributions.v1.LqtPoolSizeByEpochResponse", len)?; + if self.epoch_index != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("epochIndex", ToString::to_string(&self.epoch_index).as_str())?; + } + if let Some(v) = self.pool_size.as_ref() { + struct_ser.serialize_field("poolSize", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for LqtPoolSizeByEpochResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "epoch_index", + "epochIndex", + "pool_size", + "poolSize", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + EpochIndex, + PoolSize, + __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 { + "epochIndex" | "epoch_index" => Ok(GeneratedField::EpochIndex), + "poolSize" | "pool_size" => Ok(GeneratedField::PoolSize), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = LqtPoolSizeByEpochResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.distributions.v1.LqtPoolSizeByEpochResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut epoch_index__ = None; + let mut pool_size__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::EpochIndex => { + if epoch_index__.is_some() { + return Err(serde::de::Error::duplicate_field("epochIndex")); + } + epoch_index__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::PoolSize => { + if pool_size__.is_some() { + return Err(serde::de::Error::duplicate_field("poolSize")); + } + pool_size__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(LqtPoolSizeByEpochResponse { + epoch_index: epoch_index__.unwrap_or_default(), + pool_size: pool_size__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.distributions.v1.LqtPoolSizeByEpochResponse", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 8dbdfb069e..6dcf6253ae 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/proto/penumbra/penumbra/core/component/distributions/v1/distributions.proto b/proto/penumbra/penumbra/core/component/distributions/v1/distributions.proto index d91096d975..de977853b4 100644 --- a/proto/penumbra/penumbra/core/component/distributions/v1/distributions.proto +++ b/proto/penumbra/penumbra/core/component/distributions/v1/distributions.proto @@ -1,5 +1,12 @@ syntax = "proto3"; package penumbra.core.component.distributions.v1; +import "penumbra/core/num/v1/num.proto"; + +// Query operations for the distributions component. +service DistributionsService { + rpc CurrentLqtPoolSize(CurrentLqtPoolSizeRequest) returns (CurrentLqtPoolSizeResponse); + rpc LqtPoolSizeByEpoch(LqtPoolSizeByEpochRequest) returns (LqtPoolSizeByEpochResponse); +} // Distribution configuration data. message DistributionsParameters { @@ -13,3 +20,30 @@ message DistributionsParameters { message GenesisContent { DistributionsParameters distributions_params = 1; } + +// Request for retrieving the pool size of the current epoch from the chain state. +message CurrentLqtPoolSizeRequest {} + +// Response containing the pool size for the current epoch. +message CurrentLqtPoolSizeResponse { + // The current epoch index. + uint64 epoch_index = 1; + + // The total LQT pool size for the current epoch. + core.num.v1.Amount pool_size = 2; +} + +// Request for retrieving the pool size at a specific epoch. +message LqtPoolSizeByEpochRequest { + // The epoch for which we want to retrieve the pool size. + uint64 epoch = 1; +} + +// Response containing the pool size at a specific epoch. +message LqtPoolSizeByEpochResponse { + // The epoch for which the pool size is returned. + uint64 epoch_index = 1; + + // The total LQT pool size for the given epoch. + core.num.v1.Amount pool_size = 2; +}