From ea20ade9820cf61f1638603df5510f1459b3e249 Mon Sep 17 00:00:00 2001 From: Tal Derei <70081547+TalDerei@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:20:14 -0800 Subject: [PATCH] sct: extend commitment source with LQT variant (#5024) ## Describe your changes - new commitment source kind to track the providence of LQT reward notes ## Issue ticket number and link references https://github.com/penumbra-zone/penumbra/issues/5011 ## Checklist before requesting a review - [x] I have added guiding text to explain how a reviewer should test these changes. - [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: --- Cargo.lock | 1 + crates/bin/pcli/src/command/view/balance.rs | 6 + crates/core/component/sct/Cargo.toml | 1 + crates/core/component/sct/src/source.rs | 22 +++ .../src/gen/penumbra.core.component.sct.v1.rs | 28 +++- .../penumbra.core.component.sct.v1.serde.rs | 130 ++++++++++++++++++ .../penumbra/core/component/sct/v1/sct.proto | 9 ++ 7 files changed, 196 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b5368a3a06..ff9ae8718c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5609,6 +5609,7 @@ dependencies = [ "penumbra-sdk-keys", "penumbra-sdk-proto", "penumbra-sdk-tct", + "penumbra-sdk-txhash", "poseidon377", "rand", "rand_core", diff --git a/crates/bin/pcli/src/command/view/balance.rs b/crates/bin/pcli/src/command/view/balance.rs index 97d498a3da..020b22b19f 100644 --- a/crates/bin/pcli/src/command/view/balance.rs +++ b/crates/bin/pcli/src/command/view/balance.rs @@ -116,6 +116,12 @@ fn format_source(source: &CommitmentSource) -> String { "ICS20 packet {} via {} from {}", packet_seq, channel_id, sender ), + CommitmentSource::LiquidityTournamentReward { epoch, tx_hash } => { + format!( + "Liquidity tournament reward (Epoch {}, Tx {})", + epoch, tx_hash + ) + } } } diff --git a/crates/core/component/sct/Cargo.toml b/crates/core/component/sct/Cargo.toml index 90a59dc93f..98a5ad7de3 100644 --- a/crates/core/component/sct/Cargo.toml +++ b/crates/core/component/sct/Cargo.toml @@ -41,6 +41,7 @@ pbjson-types = {workspace = true} penumbra-sdk-keys = {workspace = true, default-features = false} penumbra-sdk-proto = {workspace = true, default-features = false} penumbra-sdk-tct = {workspace = true, default-features = true} +penumbra-sdk-txhash = {workspace = true, default-features = false} poseidon377 = {workspace = true, features = ["r1cs"]} rand = {workspace = true} rand_core = {workspace = true, features = ["getrandom"]} diff --git a/crates/core/component/sct/src/source.rs b/crates/core/component/sct/src/source.rs index 39af1ca0c7..fa49916bdb 100644 --- a/crates/core/component/sct/src/source.rs +++ b/crates/core/component/sct/src/source.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; use penumbra_sdk_proto::{core::component::sct::v1 as pb, DomainType}; +use penumbra_sdk_txhash::TransactionId; use serde::{Deserialize, Serialize}; #[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] @@ -28,6 +29,13 @@ pub enum CommitmentSource { /// The sender address on the counterparty chain. sender: String, }, + /// The commitment was created through the participation in the liquidity tournament. + LiquidityTournamentReward { + /// The epoch in which the reward occured. + epoch: u64, + /// Transaction hash of the transaction that did the voting. + tx_hash: TransactionId, + }, } impl DomainType for CommitmentSource { @@ -82,6 +90,12 @@ impl From for pb::CommitmentSource { channel_id, sender, }), + CommitmentSource::LiquidityTournamentReward { epoch, tx_hash } => { + Source::Lqt(pbcs::LiquidityTournamentReward { + epoch, + tx_hash: Some(tx_hash.into()), + }) + } }), } } @@ -116,6 +130,14 @@ impl TryFrom for CommitmentSource { channel_id: x.channel_id, sender: x.sender, }, + Source::Lqt(x) => Self::LiquidityTournamentReward { + epoch: x.epoch, + tx_hash: x + .tx_hash + .map(|x| x.try_into()) + .transpose()? + .ok_or_else(|| anyhow!("missing LQT transaction hash"))?, + }, }) } } diff --git a/crates/proto/src/gen/penumbra.core.component.sct.v1.rs b/crates/proto/src/gen/penumbra.core.component.sct.v1.rs index 3d111b51d0..0b50ca2ca1 100644 --- a/crates/proto/src/gen/penumbra.core.component.sct.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.sct.v1.rs @@ -59,7 +59,7 @@ impl ::prost::Name for Epoch { /// decide whether or not to download block data. #[derive(Clone, PartialEq, ::prost::Message)] pub struct CommitmentSource { - #[prost(oneof = "commitment_source::Source", tags = "1, 2, 20, 30, 40")] + #[prost(oneof = "commitment_source::Source", tags = "1, 2, 20, 30, 40, 50")] pub source: ::core::option::Option, } /// Nested message and enum types in `CommitmentSource`. @@ -154,6 +154,30 @@ pub mod commitment_source { "/penumbra.core.component.sct.v1.CommitmentSource.Ics20Transfer".into() } } + /// The commitment was created by the LQT mechanism and tracks LQT reward notes. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct LiquidityTournamentReward { + /// The epoch in which the reward occured. + #[prost(uint64, tag = "1")] + pub epoch: u64, + /// Transaction hash of the transaction that did the voting. + #[prost(message, optional, tag = "2")] + pub tx_hash: ::core::option::Option< + super::super::super::super::txhash::v1::TransactionId, + >, + } + impl ::prost::Name for LiquidityTournamentReward { + const NAME: &'static str = "LiquidityTournamentReward"; + const PACKAGE: &'static str = "penumbra.core.component.sct.v1"; + fn full_name() -> ::prost::alloc::string::String { + "penumbra.core.component.sct.v1.CommitmentSource.LiquidityTournamentReward" + .into() + } + fn type_url() -> ::prost::alloc::string::String { + "/penumbra.core.component.sct.v1.CommitmentSource.LiquidityTournamentReward" + .into() + } + } #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Source { #[prost(message, tag = "1")] @@ -166,6 +190,8 @@ pub mod commitment_source { CommunityPoolOutput(CommunityPoolOutput), #[prost(message, tag = "40")] Genesis(Genesis), + #[prost(message, tag = "50")] + Lqt(LiquidityTournamentReward), } } impl ::prost::Name for CommitmentSource { diff --git a/crates/proto/src/gen/penumbra.core.component.sct.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.sct.v1.serde.rs index 064ca3d29f..dbcb59dc39 100644 --- a/crates/proto/src/gen/penumbra.core.component.sct.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.sct.v1.serde.rs @@ -221,6 +221,9 @@ impl serde::Serialize for CommitmentSource { commitment_source::Source::Genesis(v) => { struct_ser.serialize_field("genesis", v)?; } + commitment_source::Source::Lqt(v) => { + struct_ser.serialize_field("lqt", v)?; + } } } struct_ser.end() @@ -241,6 +244,7 @@ impl<'de> serde::Deserialize<'de> for CommitmentSource { "community_pool_output", "communityPoolOutput", "genesis", + "lqt", ]; #[allow(clippy::enum_variant_names)] @@ -250,6 +254,7 @@ impl<'de> serde::Deserialize<'de> for CommitmentSource { FundingStreamReward, CommunityPoolOutput, Genesis, + Lqt, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -277,6 +282,7 @@ impl<'de> serde::Deserialize<'de> for CommitmentSource { "fundingStreamReward" | "funding_stream_reward" => Ok(GeneratedField::FundingStreamReward), "communityPoolOutput" | "community_pool_output" => Ok(GeneratedField::CommunityPoolOutput), "genesis" => Ok(GeneratedField::Genesis), + "lqt" => Ok(GeneratedField::Lqt), _ => Ok(GeneratedField::__SkipField__), } } @@ -332,6 +338,13 @@ impl<'de> serde::Deserialize<'de> for CommitmentSource { return Err(serde::de::Error::duplicate_field("genesis")); } source__ = map_.next_value::<::std::option::Option<_>>()?.map(commitment_source::Source::Genesis) +; + } + GeneratedField::Lqt => { + if source__.is_some() { + return Err(serde::de::Error::duplicate_field("lqt")); + } + source__ = map_.next_value::<::std::option::Option<_>>()?.map(commitment_source::Source::Lqt) ; } GeneratedField::__SkipField__ => { @@ -726,6 +739,123 @@ impl<'de> serde::Deserialize<'de> for commitment_source::Ics20Transfer { deserializer.deserialize_struct("penumbra.core.component.sct.v1.CommitmentSource.Ics20Transfer", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for commitment_source::LiquidityTournamentReward { + #[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; + } + if self.tx_hash.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.sct.v1.CommitmentSource.LiquidityTournamentReward", 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())?; + } + if let Some(v) = self.tx_hash.as_ref() { + struct_ser.serialize_field("txHash", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for commitment_source::LiquidityTournamentReward { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "epoch", + "tx_hash", + "txHash", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Epoch, + TxHash, + __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), + "txHash" | "tx_hash" => Ok(GeneratedField::TxHash), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = commitment_source::LiquidityTournamentReward; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.sct.v1.CommitmentSource.LiquidityTournamentReward") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut epoch__ = None; + let mut tx_hash__ = 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::TxHash => { + if tx_hash__.is_some() { + return Err(serde::de::Error::duplicate_field("txHash")); + } + tx_hash__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(commitment_source::LiquidityTournamentReward { + epoch: epoch__.unwrap_or_default(), + tx_hash: tx_hash__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.sct.v1.CommitmentSource.LiquidityTournamentReward", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for commitment_source::Transaction { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/proto/penumbra/penumbra/core/component/sct/v1/sct.proto b/proto/penumbra/penumbra/core/component/sct/v1/sct.proto index 742b69216c..26e7f9c6f9 100644 --- a/proto/penumbra/penumbra/core/component/sct/v1/sct.proto +++ b/proto/penumbra/penumbra/core/component/sct/v1/sct.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package penumbra.core.component.sct.v1; import "penumbra/crypto/tct/v1/tct.proto"; +import "penumbra/core/txhash/v1/txhash.proto"; import "google/protobuf/timestamp.proto"; // Configuration data for the SCT component. @@ -59,12 +60,20 @@ message CommitmentSource { // The sender address on the counterparty chain string sender = 3; } + // The commitment was created by the LQT mechanism and tracks LQT reward notes. + message LiquidityTournamentReward { + // The epoch in which the reward occured. + uint64 epoch = 1; + // Transaction hash of the transaction that did the voting. + txhash.v1.TransactionId tx_hash = 2; + } oneof source { Transaction transaction = 1; Ics20Transfer ics_20_transfer = 2; FundingStreamReward funding_stream_reward = 20; CommunityPoolOutput community_pool_output = 30; Genesis genesis = 40; + LiquidityTournamentReward lqt = 50; } }