Skip to content

Commit 6097a52

Browse files
committed
Implement check_and_execute (sans nullifier check) for LQT
1 parent 551c987 commit 6097a52

File tree

7 files changed

+109
-8
lines changed

7 files changed

+109
-8
lines changed

crates/core/component/funding/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ component = [
1414
"penumbra-sdk-proto/cnidarium",
1515
"penumbra-sdk-community-pool/component",
1616
"penumbra-sdk-distributions/component",
17+
"penumbra-sdk-governance/component",
1718
"penumbra-sdk-sct/component",
1819
"penumbra-sdk-shielded-pool/component",
1920
"penumbra-sdk-stake/component",

crates/core/component/funding/src/action_handler/liquidity_tournament/mod.rs

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
use anyhow::Context as _;
1+
use anyhow::{anyhow, Context as _};
22
use async_trait::async_trait;
3-
use cnidarium::StateWrite;
4-
use penumbra_sdk_asset::asset::Denom;
3+
use cnidarium::{StateRead, StateWrite};
4+
use cnidarium_component::ActionHandler;
5+
use penumbra_sdk_asset::{asset::Denom, Value};
6+
use penumbra_sdk_governance::StateReadExt as _;
7+
use penumbra_sdk_num::Amount;
58
use penumbra_sdk_proof_params::DELEGATOR_VOTE_PROOF_VERIFICATION_KEY;
9+
use penumbra_sdk_sct::component::clock::EpochRead as _;
10+
use penumbra_sdk_sct::epoch::Epoch;
11+
use penumbra_sdk_stake::component::validator_handler::ValidatorDataRead as _;
12+
use penumbra_sdk_tct::Position;
613
use penumbra_sdk_txhash::TransactionContext;
714

15+
use crate::component::liquidity_tournament::votes::StateWriteExt as _;
816
use crate::liquidity_tournament::{
917
proof::LiquidityTournamentVoteProofPublic, ActionLiquidityTournamentVote,
1018
LiquidityTournamentVoteBody, LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES,
1119
};
12-
use cnidarium_component::ActionHandler;
1320

1421
fn is_valid_denom(denom: &Denom) -> anyhow::Result<()> {
1522
anyhow::ensure!(
@@ -26,6 +33,37 @@ fn is_valid_denom(denom: &Denom) -> anyhow::Result<()> {
2633
Ok(())
2734
}
2835

36+
// Check that the start position is early enough to vote in the current epoch.
37+
async fn start_position_good_for_epoch(epoch: Epoch, start: Position) -> anyhow::Result<()> {
38+
anyhow::ensure!(
39+
epoch.index > u64::from(start.epoch()),
40+
"position {start:?} is not before epoch {epoch:?}"
41+
);
42+
Ok(())
43+
}
44+
45+
/// Fetch the unbonded equivalent of some purported delegation token.
46+
///
47+
/// Will fail if (either):
48+
/// - the token is not for a known validator,
49+
/// - the validator does not have any rate data.
50+
async fn unbonded_amount(state: impl StateRead, value: Value) -> anyhow::Result<Amount> {
51+
let validator = state.validator_by_delegation_asset(value.asset_id).await?;
52+
let rate = state
53+
.get_validator_rate(&validator)
54+
.await?
55+
.ok_or_else(|| anyhow!("{} has no rate data", &validator))?;
56+
Ok(rate.unbonded_amount(value.amount))
57+
}
58+
59+
// This isolates the logic for how we should handle out of bounds amounts.
60+
fn voting_power(amount: Amount) -> u64 {
61+
amount
62+
.value()
63+
.try_into()
64+
.expect("someone acquired {amount:?} > u64::MAX worth of delegation tokens!")
65+
}
66+
2967
#[async_trait]
3068
impl ActionHandler for ActionLiquidityTournamentVote {
3169
type CheckStatelessContext = TransactionContext;
@@ -70,7 +108,28 @@ impl ActionHandler for ActionLiquidityTournamentVote {
70108
Ok(())
71109
}
72110

73-
async fn check_and_execute<S: StateWrite>(&self, _state: S) -> anyhow::Result<()> {
74-
todo!()
111+
async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> anyhow::Result<()> {
112+
// 1. Check that the nullifier hasn't appeared in this round. (TODO)
113+
// 2. Check that the start position can vote in this round.
114+
let current_epoch = state
115+
.get_current_epoch()
116+
.await
117+
.expect("failed to fetch current epoch");
118+
start_position_good_for_epoch(current_epoch, self.body.start_position).await?;
119+
// 3. Tally.
120+
let power = voting_power(unbonded_amount(&state, self.body.value).await?);
121+
let incentivized = self
122+
.body
123+
.incentivized_id()
124+
.ok_or_else(|| anyhow!("{:?} is not a base denom", self.body.incentivized))?;
125+
state
126+
.tally(
127+
current_epoch.index,
128+
incentivized,
129+
power,
130+
&self.body.rewards_recipient,
131+
)
132+
.await?;
133+
Ok(())
75134
}
76135
}

crates/core/component/funding/src/component.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod state_key;
33
pub mod view;
44
use ::metrics::{gauge, histogram};
55
pub use metrics::register_metrics;
6+
pub(crate) mod liquidity_tournament;
67

78
/* Component implementation */
89
use penumbra_sdk_asset::{Value, STAKING_TOKEN_ASSET_ID};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod nullifier;
2+
pub mod votes;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use async_trait::async_trait;
2+
use cnidarium::StateWrite;
3+
use penumbra_sdk_asset::asset;
4+
use penumbra_sdk_keys::Address;
5+
6+
use crate::component::state_key;
7+
8+
#[async_trait]
9+
pub trait StateWriteExt: StateWrite {
10+
// Keeping this as returning a result to not have to touch other code if it changes to return an error.
11+
async fn tally(
12+
&mut self,
13+
epoch: u64,
14+
asset: asset::Id,
15+
power: u64,
16+
voter: &Address,
17+
) -> anyhow::Result<()> {
18+
self.nonverifiable_put_raw(
19+
state_key::lqt::v1::votes::receipt(epoch, asset, power, voter).to_vec(),
20+
Vec::default(),
21+
);
22+
Ok(())
23+
}
24+
}
25+
26+
impl<T: StateWrite + ?Sized> StateWriteExt for T {}

crates/core/component/funding/src/component/state_key.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ pub mod lqt {
4242
const POWER_LEN: usize = 8;
4343
const RECEIPT_LEN: usize = PREFIX_LEN + ASSET_LEN + POWER_LEN + ADDRESS_LEN_BYTES;
4444

45-
#[allow(dead_code)] // TODO: remove once used
4645
/// When present, indicates that an address voted for a particular asset, with a given power.
4746
///
4847
/// To get the values ordered by descending voting power, use [`prefix`];

crates/core/component/funding/src/liquidity_tournament/action/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use anyhow::{anyhow, Context};
22
use decaf377_rdsa::{Signature, SpendAuth, VerificationKey};
3-
use penumbra_sdk_asset::{asset::Denom, balance, Value};
3+
use penumbra_sdk_asset::{
4+
asset::{self, Denom, REGISTRY},
5+
balance, Value,
6+
};
47
use penumbra_sdk_keys::Address;
58
use penumbra_sdk_proto::{core::component::funding::v1 as pb, DomainType};
69
use penumbra_sdk_sct::Nullifier;
@@ -34,6 +37,17 @@ pub struct LiquidityTournamentVoteBody {
3437
pub rk: VerificationKey<SpendAuth>,
3538
}
3639

40+
impl LiquidityTournamentVoteBody {
41+
/// Get the asset id that should be incentivized.
42+
///
43+
/// This will return None if the denom is not a base denom.
44+
pub fn incentivized_id(&self) -> Option<asset::Id> {
45+
REGISTRY
46+
.parse_denom(&self.incentivized.denom)
47+
.map(|x| x.id())
48+
}
49+
}
50+
3751
impl DomainType for LiquidityTournamentVoteBody {
3852
type Proto = pb::LiquidityTournamentVoteBody;
3953
}

0 commit comments

Comments
 (0)