Skip to content

Commit 9c7da1f

Browse files
cronokirbyErwan Or
andauthored
Implement LQT Voting action, and stateless checks (#5027)
## Describe your changes Closes #5014. This implements the action for LQT voting, up to (and including) stateless checks. This is similar to delegator voting, but with some light simplification. This also reuses the strategy of UndelegateClaim in using an inner proof shared with a different circuit, which is, imo, cleaner code since the fact that the delegator vote circuit can be reused is a happy coincidence. Testing deferred. ## 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: > you betcha --------- Signed-off-by: Lúcás Meier <cronokirby@gmail.com> Co-authored-by: Erwan Or <erwanor@penumbralabs.xyz>
1 parent eff0c94 commit 9c7da1f

File tree

26 files changed

+1574
-8
lines changed

26 files changed

+1574
-8
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bin/pcli/src/transaction_view_ext.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ impl TransactionViewExt for TransactionView {
458458
action = format!("{} -> [{}]", x.action.auction_id, inside);
459459
["Dutch Auction Withdraw", &action]
460460
}
461+
penumbra_sdk_transaction::ActionView::ActionLiquidityTournamentVote(_) => todo!(),
461462
};
462463

463464
actions_table.add_row(row);

crates/core/app/src/action_handler/actions.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ impl AppActionHandler for Action {
5656
Action::ActionDutchAuctionSchedule(action) => action.check_stateless(()).await,
5757
Action::ActionDutchAuctionEnd(action) => action.check_stateless(()).await,
5858
Action::ActionDutchAuctionWithdraw(action) => action.check_stateless(()).await,
59+
Action::ActionLiquidityTournamentVote(_action) => todo!(),
5960
}
6061
}
6162

@@ -97,6 +98,7 @@ impl AppActionHandler for Action {
9798
Action::ActionDutchAuctionSchedule(action) => action.check_historical(state).await,
9899
Action::ActionDutchAuctionEnd(action) => action.check_historical(state).await,
99100
Action::ActionDutchAuctionWithdraw(action) => action.check_historical(state).await,
101+
Action::ActionLiquidityTournamentVote(_action) => todo!(),
100102
}
101103
}
102104

@@ -138,6 +140,7 @@ impl AppActionHandler for Action {
138140
Action::ActionDutchAuctionSchedule(action) => action.check_and_execute(state).await,
139141
Action::ActionDutchAuctionEnd(action) => action.check_and_execute(state).await,
140142
Action::ActionDutchAuctionWithdraw(action) => action.check_and_execute(state).await,
143+
Action::ActionLiquidityTournamentVote(_action) => todo!(),
141144
}
142145
}
143146
}

crates/core/asset/src/asset/denom.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
44
///
55
/// Each denomination has a unique [`asset::Id`] and base unit, and may also
66
/// have other display units.
7-
#[derive(Serialize, Deserialize, Clone)]
7+
#[derive(Serialize, Deserialize, Clone, Debug)]
88
#[serde(try_from = "pb::Denom", into = "pb::Denom")]
99
pub struct Denom {
1010
pub denom: String,

crates/core/component/funding/Cargo.toml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,40 @@ component = [
2121
"futures"
2222
]
2323
default = ["component"]
24+
parallel = [
25+
"ark-groth16/parallel",
26+
"decaf377/parallel",
27+
"decaf377-rdsa/parallel",
28+
"penumbra-sdk-tct/parallel",
29+
"penumbra-sdk-shielded-pool/parallel",
30+
"penumbra-sdk-governance/parallel"
31+
]
2432
docsrs = []
2533

2634
[dependencies]
2735
anyhow = {workspace = true}
36+
ark-groth16 = {workspace = true, default-features = false}
2837
async-trait = {workspace = true}
38+
base64 = {workspace = true}
2939
cnidarium = {workspace = true, optional = true, default-features = true}
3040
cnidarium-component = {workspace = true, optional = true, default-features = true}
41+
decaf377 = {workspace = true}
42+
decaf377-rdsa = {workspace = true}
3143
futures = {workspace = true, optional = true}
3244
metrics = {workspace = true, optional = true}
3345
penumbra-sdk-asset = {workspace = true, default-features = true}
3446
penumbra-sdk-community-pool = {workspace = true, default-features = false}
3547
penumbra-sdk-distributions = {workspace = true, default-features = false}
48+
penumbra-sdk-governance = {workspace = true, default-features = false}
49+
penumbra-sdk-keys = {workspace = true, default-features = false}
50+
penumbra-sdk-num = {workspace = true, default-features = false}
51+
penumbra-sdk-proof-params = {workspace = true, default-features = false}
3652
penumbra-sdk-proto = {workspace = true, default-features = false}
3753
penumbra-sdk-sct = {workspace = true, default-features = false}
38-
penumbra-sdk-num = {workspace = true, default-features = false}
3954
penumbra-sdk-shielded-pool = {workspace = true, default-features = false}
4055
penumbra-sdk-stake = {workspace = true, default-features = false}
56+
penumbra-sdk-tct = {workspace = true, default-features = false}
57+
penumbra-sdk-txhash = {workspace = true, default-features = false}
4158
serde = {workspace = true, features = ["derive"]}
4259
tendermint = {workspace = true}
4360
tracing = {workspace = true}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use anyhow::Context as _;
2+
use async_trait::async_trait;
3+
use cnidarium::StateWrite;
4+
use penumbra_sdk_asset::asset::Denom;
5+
use penumbra_sdk_proof_params::DELEGATOR_VOTE_PROOF_VERIFICATION_KEY;
6+
use penumbra_sdk_txhash::TransactionContext;
7+
8+
use crate::liquidity_tournament::{
9+
proof::LiquidityTournamentVoteProofPublic, ActionLiquidityTournamentVote,
10+
LiquidityTournamentVoteBody, LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES,
11+
};
12+
use cnidarium_component::ActionHandler;
13+
14+
fn is_valid_denom(denom: &Denom) -> anyhow::Result<()> {
15+
anyhow::ensure!(
16+
denom.denom.len() <= LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES,
17+
"denom {} is not <= (MAX OF) {}",
18+
&denom.denom,
19+
LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES
20+
);
21+
anyhow::ensure!(
22+
denom.denom.starts_with("transfer/"),
23+
"denom {} is not an IBC transfer asset",
24+
&denom.denom
25+
);
26+
Ok(())
27+
}
28+
29+
#[async_trait]
30+
impl ActionHandler for ActionLiquidityTournamentVote {
31+
type CheckStatelessContext = TransactionContext;
32+
33+
async fn check_stateless(&self, context: TransactionContext) -> anyhow::Result<()> {
34+
let Self {
35+
auth_sig,
36+
proof,
37+
body:
38+
LiquidityTournamentVoteBody {
39+
start_position,
40+
nullifier,
41+
rk,
42+
value,
43+
incentivized,
44+
..
45+
},
46+
} = self;
47+
// 1. Is it ok to vote on this denom?
48+
is_valid_denom(incentivized)?;
49+
// 2. Check spend auth signature using provided spend auth key.
50+
rk.verify(context.effect_hash.as_ref(), auth_sig)
51+
.with_context(|| {
52+
format!(
53+
"{} auth signature failed to verify",
54+
std::any::type_name::<Self>()
55+
)
56+
})?;
57+
58+
// 3. Verify the proof against the provided anchor and start position:
59+
let public = LiquidityTournamentVoteProofPublic {
60+
anchor: context.anchor,
61+
value: *value,
62+
nullifier: *nullifier,
63+
rk: *rk,
64+
start_position: *start_position,
65+
};
66+
proof
67+
.verify(&DELEGATOR_VOTE_PROOF_VERIFICATION_KEY, public)
68+
.context("a LiquidityTournamentVote proof did not verify")?;
69+
70+
Ok(())
71+
}
72+
73+
async fn check_and_execute<S: StateWrite>(&self, _state: S) -> anyhow::Result<()> {
74+
todo!()
75+
}
76+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod liquidity_tournament;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
#![deny(clippy::unwrap_used)]
22
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
33
#[cfg(feature = "component")]
4+
pub mod action_handler;
5+
#[cfg(feature = "component")]
46
pub mod component;
57
pub mod event;
68

79
pub mod genesis;
10+
pub mod liquidity_tournament;
811
pub mod params;
912
pub use params::FundingParameters;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use anyhow::{anyhow, Context};
2+
use decaf377_rdsa::{Signature, SpendAuth, VerificationKey};
3+
use penumbra_sdk_asset::{asset::Denom, balance, Value};
4+
use penumbra_sdk_keys::Address;
5+
use penumbra_sdk_proto::{core::component::funding::v1 as pb, DomainType};
6+
use penumbra_sdk_sct::Nullifier;
7+
use penumbra_sdk_tct::Position;
8+
use penumbra_sdk_txhash::{EffectHash, EffectingData};
9+
10+
use super::proof::LiquidityTournamentVoteProof;
11+
12+
/// The internal body of an LQT vote, containing the intended vote and other validity information.
13+
///
14+
/// c.f. [`penumbra_sdk_governance::delegator_vote::action::DelegatorVoteBody`], which is similar.
15+
#[derive(Clone, Debug)]
16+
pub struct LiquidityTournamentVoteBody {
17+
/// Which asset is being incentivized.
18+
///
19+
/// We use the base denom to allow filtering particular asset sources (i.e. IBC transfers)a.
20+
pub incentivized: Denom,
21+
/// The address that will receive any rewards for voting.
22+
pub rewards_recipient: Address,
23+
/// The start position of the tournament.
24+
///
25+
/// This is included to allow stateless verification, but should match the epoch of the LQT.
26+
pub start_position: Position,
27+
/// The value being used to vote with.
28+
///
29+
/// This should be the delegation tokens for a specific validator.
30+
pub value: Value,
31+
/// The nullifier of the note being spent.
32+
pub nullifier: Nullifier,
33+
/// The key that must be used to vote.
34+
pub rk: VerificationKey<SpendAuth>,
35+
}
36+
37+
impl DomainType for LiquidityTournamentVoteBody {
38+
type Proto = pb::LiquidityTournamentVoteBody;
39+
}
40+
41+
impl TryFrom<pb::LiquidityTournamentVoteBody> for LiquidityTournamentVoteBody {
42+
type Error = anyhow::Error;
43+
44+
fn try_from(proto: pb::LiquidityTournamentVoteBody) -> Result<Self, Self::Error> {
45+
Result::<_, Self::Error>::Ok(Self {
46+
incentivized: proto
47+
.incentivized
48+
.ok_or_else(|| anyhow!("missing `incentivized`"))?
49+
.try_into()?,
50+
rewards_recipient: proto
51+
.rewards_recipient
52+
.ok_or_else(|| anyhow!("missing `rewards_recipient`"))?
53+
.try_into()?,
54+
start_position: proto.start_position.into(),
55+
value: proto
56+
.value
57+
.ok_or_else(|| anyhow!("missing `value`"))?
58+
.try_into()?,
59+
nullifier: proto
60+
.nullifier
61+
.ok_or_else(|| anyhow!("missing `nullifier`"))?
62+
.try_into()?,
63+
rk: proto
64+
.rk
65+
.ok_or_else(|| anyhow!("missing `rk`"))?
66+
.try_into()?,
67+
})
68+
.with_context(|| format!("while parsing {}", std::any::type_name::<Self>()))
69+
}
70+
}
71+
72+
impl From<LiquidityTournamentVoteBody> for pb::LiquidityTournamentVoteBody {
73+
fn from(value: LiquidityTournamentVoteBody) -> Self {
74+
Self {
75+
incentivized: Some(value.incentivized.into()),
76+
rewards_recipient: Some(value.rewards_recipient.into()),
77+
start_position: value.start_position.into(),
78+
value: Some(value.value.into()),
79+
nullifier: Some(value.nullifier.into()),
80+
rk: Some(value.rk.into()),
81+
}
82+
}
83+
}
84+
85+
/// The action used to vote in the liquidity tournament.
86+
///
87+
/// This vote is towards a particular asset whose liquidity should be incentivized,
88+
/// and is weighted by the amount of delegation tokens being expended.
89+
#[derive(Clone, Debug)]
90+
pub struct ActionLiquidityTournamentVote {
91+
/// The actual body, containing the vote and other validity information.
92+
pub body: LiquidityTournamentVoteBody,
93+
/// An authorization over the body.
94+
pub auth_sig: Signature<SpendAuth>,
95+
/// A ZK proof tying in the private information for this action.
96+
pub proof: LiquidityTournamentVoteProof,
97+
}
98+
99+
impl DomainType for ActionLiquidityTournamentVote {
100+
type Proto = pb::ActionLiquidityTournamentVote;
101+
}
102+
103+
impl TryFrom<pb::ActionLiquidityTournamentVote> for ActionLiquidityTournamentVote {
104+
type Error = anyhow::Error;
105+
106+
fn try_from(value: pb::ActionLiquidityTournamentVote) -> Result<Self, Self::Error> {
107+
Result::<_, Self::Error>::Ok(Self {
108+
body: value
109+
.body
110+
.ok_or_else(|| anyhow!("missing `body`"))?
111+
.try_into()?,
112+
auth_sig: value
113+
.auth_sig
114+
.ok_or_else(|| anyhow!("missing `auth_sig`"))?
115+
.try_into()?,
116+
proof: value
117+
.proof
118+
.ok_or_else(|| anyhow!("missing `proof`"))?
119+
.try_into()?,
120+
})
121+
.with_context(|| format!("while parsing {}", std::any::type_name::<Self>()))
122+
}
123+
}
124+
125+
impl From<ActionLiquidityTournamentVote> for pb::ActionLiquidityTournamentVote {
126+
fn from(value: ActionLiquidityTournamentVote) -> Self {
127+
Self {
128+
body: Some(value.body.into()),
129+
auth_sig: Some(value.auth_sig.into()),
130+
proof: Some(value.proof.into()),
131+
}
132+
}
133+
}
134+
135+
impl EffectingData for ActionLiquidityTournamentVote {
136+
fn effect_hash(&self) -> EffectHash {
137+
EffectHash::from_proto_effecting_data(&self.to_proto())
138+
}
139+
}
140+
141+
impl ActionLiquidityTournamentVote {
142+
/// This action doesn't actually produce or consume value.
143+
pub fn balance_commitment(&self) -> balance::Commitment {
144+
// This will be the commitment to zero.
145+
balance::Commitment::default()
146+
}
147+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
mod action;
2+
mod view;
3+
4+
pub mod proof;
5+
pub use action::{ActionLiquidityTournamentVote, LiquidityTournamentVoteBody};
6+
pub use view::ActionLiquidityTournamentVoteView;
7+
8+
/// The maximum number of allowable bytes in the denom string.
9+
pub const LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES: usize = 256;

0 commit comments

Comments
 (0)