diff --git a/crates/core/component/shielded-pool/src/component/shielded_pool.rs b/crates/core/component/shielded-pool/src/component/shielded_pool.rs index 4c0c809fac..4ae8cdd963 100644 --- a/crates/core/component/shielded-pool/src/component/shielded_pool.rs +++ b/crates/core/component/shielded-pool/src/component/shielded_pool.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::fmd::should_update_fmd_params; +use crate::fmd::{should_update_fmd_params, ClueManagerInternal as _}; use crate::params::ShieldedPoolParameters; use crate::{fmd, genesis, state_key}; use anyhow::anyhow; @@ -83,7 +83,11 @@ impl Component for ShieldedPool { .get_current_fmd_parameters() .await .expect("should be able to read state"); - let new = meta_params.updated_fmd_params(&old, height); + let clue_count_delta = state + .flush_clue_count() + .await + .expect("should be able to read state"); + let new = meta_params.updated_fmd_params(&old, height, clue_count_delta); state.put_previous_fmd_parameters(old); state.put_current_fmd_parameters(new); } diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index e8e1541949..e500048aa6 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -1,9 +1,12 @@ -use anyhow::anyhow; -use decaf377_fmd::Precision; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use cnidarium::{StateRead, StateWrite}; +use decaf377_fmd::{Clue, Precision}; use penumbra_proto::{ core::component::shielded_pool::v1::{self as pb}, - DomainType, + DomainType, StateWriteProto, }; +use penumbra_txhash::TransactionId; use serde::{Deserialize, Serialize}; pub mod state_key; @@ -35,7 +38,7 @@ impl DomainType for Parameters { impl TryFrom for Parameters { type Error = anyhow::Error; - fn try_from(msg: pb::FmdParameters) -> Result { + fn try_from(msg: pb::FmdParameters) -> Result { Ok(Parameters { precision: msg.precision_bits.try_into()?, as_of_block_height: msg.as_of_block_height, @@ -72,7 +75,7 @@ pub enum MetaParameters { impl TryFrom for MetaParameters { type Error = anyhow::Error; - fn try_from(value: pb::FmdMetaParameters) -> Result { + fn try_from(value: pb::FmdMetaParameters) -> Result { match value.algorithm.ok_or(anyhow!("missing algorithm"))? { pb::fmd_meta_parameters::Algorithm::FixedPrecisionBits(p) => { Ok(MetaParameters::Fixed(Precision::new(p as u8)?)) @@ -104,7 +107,7 @@ impl Default for MetaParameters { } impl MetaParameters { - pub fn updated_fmd_params(&self, _old: &Parameters, height: u64) -> Parameters { + pub fn updated_fmd_params(&self, _old: &Parameters, height: u64, _clue_count_delta: (u64, u64)) -> Parameters { match *self { MetaParameters::Fixed(precision) => Parameters { precision, @@ -113,3 +116,84 @@ impl MetaParameters { } } } + +#[async_trait] +trait ClueWriteExt: StateWrite { + fn put_current_clue_count(&mut self, count: u64) { + self.put_raw( + state_key::clue_count::current().to_string(), + count.to_be_bytes().to_vec(), + ) + } + + fn put_previous_clue_count(&mut self, count: u64) { + self.put_raw( + state_key::clue_count::previous().to_string(), + count.to_be_bytes().to_vec(), + ) + } +} + +impl ClueWriteExt for T {} + +#[async_trait] +trait ClueReadExt: StateRead { + async fn get_current_clue_count(&self) -> Result { + Ok(u64::from_be_bytes( + self.get_raw(state_key::clue_count::current()) + .await? + .ok_or(anyhow!("no current clue count"))? + .as_slice() + .try_into()?, + )) + } + + async fn get_previous_clue_count(&self) -> Result { + Ok(u64::from_be_bytes( + self.get_raw(state_key::clue_count::previous()) + .await? + .ok_or(anyhow!("no current clue count"))? + .as_slice() + .try_into()?, + )) + } +} + +impl ClueReadExt for T {} + +#[async_trait] +pub trait ClueManager: StateRead + StateWrite { + async fn record_clue(&mut self, clue: Clue, tx: TransactionId) -> Result<()> { + // Update count + { + let count = self.get_current_clue_count().await?; + self.put_current_clue_count(count.saturating_add(1)); + } + self.record_proto(pb::EventClue { + clue: Some(clue.into()), + tx: Some(tx.into()) + }); + Ok(()) + } +} + +impl ClueManager for T {} + +#[async_trait] +pub(crate) trait ClueManagerInternal: ClueManager { + fn init(&mut self) { + self.put_current_clue_count(0); + self.put_previous_clue_count(0); + } + + /// Flush the clue counts, returning the previous and current counts + async fn flush_clue_count(&mut self) -> Result<(u64, u64)> { + let previous = self.get_previous_clue_count().await?; + let current = self.get_current_clue_count().await?; + self.put_previous_clue_count(current); + self.put_current_clue_count(0); + Ok((previous, current)) + } +} + +impl ClueManagerInternal for T {} diff --git a/crates/core/component/shielded-pool/src/fmd/state_key.rs b/crates/core/component/shielded-pool/src/fmd/state_key.rs index d1e47596f7..4a3d1fcc27 100644 --- a/crates/core/component/shielded-pool/src/fmd/state_key.rs +++ b/crates/core/component/shielded-pool/src/fmd/state_key.rs @@ -9,3 +9,13 @@ pub mod parameters { "shielded_pool/fmd_parameters/previous" } } + +pub(super) mod clue_count { + pub fn current() -> &'static str { + "shielded_pool/fmd_clue_count/current" + } + + pub fn previous() -> &'static str { + "shielded_pool/fmd_clue_count/previous" + } +} diff --git a/crates/core/component/shielded-pool/src/lib.rs b/crates/core/component/shielded-pool/src/lib.rs index 84d112ed52..96c97789df 100644 --- a/crates/core/component/shielded-pool/src/lib.rs +++ b/crates/core/component/shielded-pool/src/lib.rs @@ -26,6 +26,7 @@ pub mod output; pub mod spend; pub use convert::{ConvertCircuit, ConvertProof, ConvertProofPrivate, ConvertProofPublic}; +pub use fmd::ClueManager; pub use nullifier_derivation::{ NullifierDerivationCircuit, NullifierDerivationProof, NullifierDerivationProofPrivate, NullifierDerivationProofPublic, diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index 164f280336..88b981b3d2 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs @@ -291,6 +291,26 @@ impl ::prost::Name for EventOutput { ) } } +/// ABCI Event recording a clue. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventClue { + #[prost(message, optional, tag = "1")] + pub clue: ::core::option::Option< + super::super::super::super::crypto::decaf377_fmd::v1::Clue, + >, + #[prost(message, optional, tag = "2")] + pub tx: ::core::option::Option, +} +impl ::prost::Name for EventClue { + const NAME: &'static str = "EventClue"; + const PACKAGE: &'static str = "penumbra.core.component.shielded_pool.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.shielded_pool.v1.{}", Self::NAME + ) + } +} /// The body of a spend description, containing only the effecting data /// describing changes to the ledger, and not the authorizing data that allows /// those changes to be performed. diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs index a1e86d33d3..e86ea48fe0 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs @@ -190,6 +190,118 @@ impl<'de> serde::Deserialize<'de> for AssetMetadataByIdResponse { deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.AssetMetadataByIdResponse", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for EventClue { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.clue.is_some() { + len += 1; + } + if self.tx.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.EventClue", len)?; + if let Some(v) = self.clue.as_ref() { + struct_ser.serialize_field("clue", v)?; + } + if let Some(v) = self.tx.as_ref() { + struct_ser.serialize_field("tx", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventClue { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "clue", + "tx", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Clue, + Tx, + __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 { + "clue" => Ok(GeneratedField::Clue), + "tx" => Ok(GeneratedField::Tx), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventClue; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.EventClue") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut clue__ = None; + let mut tx__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Clue => { + if clue__.is_some() { + return Err(serde::de::Error::duplicate_field("clue")); + } + clue__ = map_.next_value()?; + } + GeneratedField::Tx => { + if tx__.is_some() { + return Err(serde::de::Error::duplicate_field("tx")); + } + tx__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventClue { + clue: clue__, + tx: tx__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventClue", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for EventOutput { #[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 634db20b2f..c122e7f4ef 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/shielded_pool/v1/shielded_pool.proto b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto index 24f4575192..0e9dc7a35c 100644 --- a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto +++ b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto @@ -5,6 +5,8 @@ import "penumbra/core/asset/v1/asset.proto"; import "penumbra/core/component/sct/v1/sct.proto"; import "penumbra/core/keys/v1/keys.proto"; import "penumbra/core/num/v1/num.proto"; +import "penumbra/core/txhash/v1/txhash.proto"; +import "penumbra/crypto/decaf377_fmd/v1/decaf377_fmd.proto"; import "penumbra/crypto/decaf377_rdsa/v1/decaf377_rdsa.proto"; import "penumbra/crypto/tct/v1/tct.proto"; @@ -104,6 +106,12 @@ message EventOutput { crypto.tct.v1.StateCommitment note_commitment = 1; } +// ABCI Event recording a clue. +message EventClue { + crypto.decaf377_fmd.v1.Clue clue = 1; + txhash.v1.TransactionId tx = 2; +} + // The body of a spend description, containing only the effecting data // describing changes to the ledger, and not the authorizing data that allows // those changes to be performed.