Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement LQT Voting action, and stateless checks #5027

Merged
merged 6 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/bin/pcli/src/transaction_view_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ impl TransactionViewExt for TransactionView {
action = format!("{} -> [{}]", x.action.auction_id, inside);
["Dutch Auction Withdraw", &action]
}
penumbra_sdk_transaction::ActionView::ActionLiquidityTournamentVote(_) => todo!(),
};

actions_table.add_row(row);
Expand Down
3 changes: 3 additions & 0 deletions crates/core/app/src/action_handler/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ impl AppActionHandler for Action {
Action::ActionDutchAuctionSchedule(action) => action.check_stateless(()).await,
Action::ActionDutchAuctionEnd(action) => action.check_stateless(()).await,
Action::ActionDutchAuctionWithdraw(action) => action.check_stateless(()).await,
Action::ActionLiquidityTournamentVote(_action) => todo!(),
}
}

Expand Down Expand Up @@ -97,6 +98,7 @@ impl AppActionHandler for Action {
Action::ActionDutchAuctionSchedule(action) => action.check_historical(state).await,
Action::ActionDutchAuctionEnd(action) => action.check_historical(state).await,
Action::ActionDutchAuctionWithdraw(action) => action.check_historical(state).await,
Action::ActionLiquidityTournamentVote(_action) => todo!(),
}
}

Expand Down Expand Up @@ -138,6 +140,7 @@ impl AppActionHandler for Action {
Action::ActionDutchAuctionSchedule(action) => action.check_and_execute(state).await,
Action::ActionDutchAuctionEnd(action) => action.check_and_execute(state).await,
Action::ActionDutchAuctionWithdraw(action) => action.check_and_execute(state).await,
Action::ActionLiquidityTournamentVote(_action) => todo!(),
}
}
}
2 changes: 1 addition & 1 deletion crates/core/asset/src/asset/denom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
///
/// Each denomination has a unique [`asset::Id`] and base unit, and may also
/// have other display units.
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(try_from = "pb::Denom", into = "pb::Denom")]
pub struct Denom {
pub denom: String,
Expand Down
19 changes: 18 additions & 1 deletion crates/core/component/funding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,40 @@ component = [
"futures"
]
default = ["component"]
parallel = [
"ark-groth16/parallel",
"decaf377/parallel",
"decaf377-rdsa/parallel",
"penumbra-sdk-tct/parallel",
"penumbra-sdk-shielded-pool/parallel",
"penumbra-sdk-governance/parallel"
]
docsrs = []

[dependencies]
anyhow = {workspace = true}
ark-groth16 = {workspace = true, default-features = false}
async-trait = {workspace = true}
base64 = {workspace = true}
cnidarium = {workspace = true, optional = true, default-features = true}
cnidarium-component = {workspace = true, optional = true, default-features = true}
decaf377 = {workspace = true}
decaf377-rdsa = {workspace = true}
futures = {workspace = true, optional = true}
metrics = {workspace = true, optional = true}
penumbra-sdk-asset = {workspace = true, default-features = true}
penumbra-sdk-community-pool = {workspace = true, default-features = false}
penumbra-sdk-distributions = {workspace = true, default-features = false}
penumbra-sdk-governance = {workspace = true, default-features = false}
penumbra-sdk-keys = {workspace = true, default-features = false}
penumbra-sdk-num = {workspace = true, default-features = false}
penumbra-sdk-proof-params = {workspace = true, default-features = false}
penumbra-sdk-proto = {workspace = true, default-features = false}
penumbra-sdk-sct = {workspace = true, default-features = false}
penumbra-sdk-num = {workspace = true, default-features = false}
penumbra-sdk-shielded-pool = {workspace = true, default-features = false}
penumbra-sdk-stake = {workspace = true, default-features = false}
penumbra-sdk-tct = {workspace = true, default-features = false}
penumbra-sdk-txhash = {workspace = true, default-features = false}
serde = {workspace = true, features = ["derive"]}
tendermint = {workspace = true}
tracing = {workspace = true}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use anyhow::Context as _;
use async_trait::async_trait;
use cnidarium::StateWrite;
use penumbra_sdk_asset::asset::Denom;
use penumbra_sdk_proof_params::DELEGATOR_VOTE_PROOF_VERIFICATION_KEY;
use penumbra_sdk_txhash::TransactionContext;

use crate::liquidity_tournament::{
proof::LiquidityTournamentVoteProofPublic, ActionLiquidityTournamentVote,
LiquidityTournamentVoteBody, LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES,
};
use cnidarium_component::ActionHandler;

fn is_valid_denom(denom: &Denom) -> anyhow::Result<()> {
anyhow::ensure!(
denom.denom.len() <= LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES,
"denom {} is not <= (MAX OF) {}",
&denom.denom,
LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES
);
anyhow::ensure!(
denom.denom.starts_with("transfer/"),
"denom {} is not an IBC transfer asset",
&denom.denom
);
Ok(())
}

#[async_trait]
impl ActionHandler for ActionLiquidityTournamentVote {
type CheckStatelessContext = TransactionContext;

async fn check_stateless(&self, context: TransactionContext) -> anyhow::Result<()> {
let Self {
auth_sig,
proof,
body:
LiquidityTournamentVoteBody {
start_position,
nullifier,
rk,
value,
incentivized,
..
},
} = self;
// 1. Is it ok to vote on this denom?
is_valid_denom(incentivized)?;
// 2. Check spend auth signature using provided spend auth key.
rk.verify(context.effect_hash.as_ref(), auth_sig)
.with_context(|| {
format!(
"{} auth signature failed to verify",
std::any::type_name::<Self>()
)
})?;

// 3. Verify the proof against the provided anchor and start position:
let public = LiquidityTournamentVoteProofPublic {
anchor: context.anchor,
value: *value,
nullifier: *nullifier,
rk: *rk,
start_position: *start_position,
};
proof
.verify(&DELEGATOR_VOTE_PROOF_VERIFICATION_KEY, public)
.context("a LiquidityTournamentVote proof did not verify")?;

Ok(())
}

async fn check_and_execute<S: StateWrite>(&self, _state: S) -> anyhow::Result<()> {
todo!()
}
}
1 change: 1 addition & 0 deletions crates/core/component/funding/src/action_handler/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod liquidity_tournament;
3 changes: 3 additions & 0 deletions crates/core/component/funding/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#![deny(clippy::unwrap_used)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[cfg(feature = "component")]
pub mod action_handler;
#[cfg(feature = "component")]
pub mod component;
pub mod event;

pub mod genesis;
pub mod liquidity_tournament;
pub mod params;
pub use params::FundingParameters;
147 changes: 147 additions & 0 deletions crates/core/component/funding/src/liquidity_tournament/action/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use anyhow::{anyhow, Context};
use decaf377_rdsa::{Signature, SpendAuth, VerificationKey};
use penumbra_sdk_asset::{asset::Denom, balance, Value};
use penumbra_sdk_keys::Address;
use penumbra_sdk_proto::{core::component::funding::v1 as pb, DomainType};
use penumbra_sdk_sct::Nullifier;
use penumbra_sdk_tct::Position;
use penumbra_sdk_txhash::{EffectHash, EffectingData};

use super::proof::LiquidityTournamentVoteProof;

/// The internal body of an LQT vote, containing the intended vote and other validity information.
///
/// c.f. [`penumbra_sdk_governance::delegator_vote::action::DelegatorVoteBody`], which is similar.
#[derive(Clone, Debug)]
pub struct LiquidityTournamentVoteBody {
/// Which asset is being incentivized.
///
/// We use the base denom to allow filtering particular asset sources (i.e. IBC transfers)a.
pub incentivized: Denom,
/// The address that will receive any rewards for voting.
pub rewards_recipient: Address,
/// The start position of the tournament.
///
/// This is included to allow stateless verification, but should match the epoch of the LQT.
pub start_position: Position,
/// The value being used to vote with.
///
/// This should be the delegation tokens for a specific validator.
pub value: Value,
/// The nullifier of the note being spent.
pub nullifier: Nullifier,
/// The key that must be used to vote.
pub rk: VerificationKey<SpendAuth>,
}

impl DomainType for LiquidityTournamentVoteBody {
type Proto = pb::LiquidityTournamentVoteBody;
}

impl TryFrom<pb::LiquidityTournamentVoteBody> for LiquidityTournamentVoteBody {
type Error = anyhow::Error;

fn try_from(proto: pb::LiquidityTournamentVoteBody) -> Result<Self, Self::Error> {
Result::<_, Self::Error>::Ok(Self {
incentivized: proto
.incentivized
.ok_or_else(|| anyhow!("missing `incentivized`"))?
.try_into()?,
rewards_recipient: proto
.rewards_recipient
.ok_or_else(|| anyhow!("missing `rewards_recipient`"))?
.try_into()?,
start_position: proto.start_position.into(),
value: proto
.value
.ok_or_else(|| anyhow!("missing `value`"))?
.try_into()?,
nullifier: proto
.nullifier
.ok_or_else(|| anyhow!("missing `nullifier`"))?
.try_into()?,
rk: proto
.rk
.ok_or_else(|| anyhow!("missing `rk`"))?
.try_into()?,
})
.with_context(|| format!("while parsing {}", std::any::type_name::<Self>()))
}
}

impl From<LiquidityTournamentVoteBody> for pb::LiquidityTournamentVoteBody {
fn from(value: LiquidityTournamentVoteBody) -> Self {
Self {
incentivized: Some(value.incentivized.into()),
rewards_recipient: Some(value.rewards_recipient.into()),
start_position: value.start_position.into(),
value: Some(value.value.into()),
nullifier: Some(value.nullifier.into()),
rk: Some(value.rk.into()),
}
}
}

/// The action used to vote in the liquidity tournament.
///
/// This vote is towards a particular asset whose liquidity should be incentivized,
/// and is weighted by the amount of delegation tokens being expended.
#[derive(Clone, Debug)]
pub struct ActionLiquidityTournamentVote {
/// The actual body, containing the vote and other validity information.
pub body: LiquidityTournamentVoteBody,
/// An authorization over the body.
pub auth_sig: Signature<SpendAuth>,
/// A ZK proof tying in the private information for this action.
pub proof: LiquidityTournamentVoteProof,
}

impl DomainType for ActionLiquidityTournamentVote {
type Proto = pb::ActionLiquidityTournamentVote;
}

impl TryFrom<pb::ActionLiquidityTournamentVote> for ActionLiquidityTournamentVote {
type Error = anyhow::Error;

fn try_from(value: pb::ActionLiquidityTournamentVote) -> Result<Self, Self::Error> {
Result::<_, Self::Error>::Ok(Self {
body: value
.body
.ok_or_else(|| anyhow!("missing `body`"))?
.try_into()?,
auth_sig: value
.auth_sig
.ok_or_else(|| anyhow!("missing `auth_sig`"))?
.try_into()?,
proof: value
.proof
.ok_or_else(|| anyhow!("missing `proof`"))?
.try_into()?,
})
.with_context(|| format!("while parsing {}", std::any::type_name::<Self>()))
}
}

impl From<ActionLiquidityTournamentVote> for pb::ActionLiquidityTournamentVote {
fn from(value: ActionLiquidityTournamentVote) -> Self {
Self {
body: Some(value.body.into()),
auth_sig: Some(value.auth_sig.into()),
proof: Some(value.proof.into()),
}
}
}

impl EffectingData for ActionLiquidityTournamentVote {
fn effect_hash(&self) -> EffectHash {
EffectHash::from_proto_effecting_data(&self.to_proto())
}
}

impl ActionLiquidityTournamentVote {
/// This action doesn't actually produce or consume value.
pub fn balance_commitment(&self) -> balance::Commitment {
// This will be the commitment to zero.
balance::Commitment::default()
}
}
9 changes: 9 additions & 0 deletions crates/core/component/funding/src/liquidity_tournament/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
mod action;
mod view;

pub mod proof;
pub use action::{ActionLiquidityTournamentVote, LiquidityTournamentVoteBody};
pub use view::ActionLiquidityTournamentVoteView;

/// The maximum number of allowable bytes in the denom string.
pub const LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES: usize = 256;
Loading