Skip to content

Commit 849cd47

Browse files
committed
Implement check and execute for LQT votes (#5033)
Closes #5032. The check and execute logic for the action handler is still missing the nullifier check, but there's an obvious insertion point for adding that logic. Testing deferred.
1 parent d8abe73 commit 849cd47

File tree

7 files changed

+175
-10
lines changed

7 files changed

+175
-10
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: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
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;
6-
use penumbra_sdk_txhash::TransactionContext;
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;
13+
use penumbra_sdk_txhash::{TransactionContext, TransactionId};
714

15+
use crate::component::liquidity_tournament::{
16+
nullifier::{NullifierRead as _, NullifierWrite as _},
17+
votes::StateWriteExt as _,
18+
};
819
use crate::liquidity_tournament::{
920
proof::LiquidityTournamentVoteProofPublic, ActionLiquidityTournamentVote,
1021
LiquidityTournamentVoteBody, LIQUIDITY_TOURNAMENT_VOTE_DENOM_MAX_BYTES,
1122
};
12-
use cnidarium_component::ActionHandler;
1323

1424
fn is_valid_denom(denom: &Denom) -> anyhow::Result<()> {
1525
anyhow::ensure!(
@@ -26,6 +36,37 @@ fn is_valid_denom(denom: &Denom) -> anyhow::Result<()> {
2636
Ok(())
2737
}
2838

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

73-
async fn check_and_execute<S: StateWrite>(&self, _state: S) -> anyhow::Result<()> {
74-
todo!()
114+
async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> anyhow::Result<()> {
115+
// 1. Check that the start position can vote in this round.
116+
let current_epoch = state
117+
.get_current_epoch()
118+
.await
119+
.expect("failed to fetch current epoch");
120+
start_position_good_for_epoch(current_epoch, self.body.start_position).await?;
121+
// 2. We can tally, as long as the nullifier hasn't been used yet (this round).
122+
let nullifier = self.body.nullifier;
123+
let nullifier_exists = state.get_lqt_spent_nullifier(nullifier).await.is_some();
124+
anyhow::ensure!(
125+
!nullifier_exists,
126+
"nullifier {} already voted in epoch {}",
127+
self.body.nullifier,
128+
current_epoch.index
129+
);
130+
state.put_lqt_spent_nullifier(current_epoch.index, nullifier, TransactionId([0u8; 32]));
131+
// 3. Ok, actually tally.
132+
let power = voting_power(unbonded_amount(&state, self.body.value).await?);
133+
let incentivized = self
134+
.body
135+
.incentivized_id()
136+
.ok_or_else(|| anyhow!("{:?} is not a base denom", self.body.incentivized))?;
137+
state
138+
.tally(
139+
current_epoch.index,
140+
incentivized,
141+
power,
142+
&self.body.rewards_recipient,
143+
)
144+
.await?;
145+
Ok(())
75146
}
76147
}
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;

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use cnidarium::{StateRead, StateWrite};
66
use penumbra_sdk_proto::{StateReadProto, StateWriteProto};
77
use penumbra_sdk_sct::{component::clock::EpochRead, Nullifier};
88

9-
#[allow(dead_code)]
109
#[async_trait]
1110
pub trait NullifierRead: StateRead {
1211
/// Returns the `TransactionId` if the nullifier has been spent; otherwise, returns None.
@@ -34,7 +33,6 @@ pub trait NullifierRead: StateRead {
3433

3534
impl<T: StateRead + ?Sized> NullifierRead for T {}
3635

37-
#[allow(dead_code)]
3836
#[async_trait]
3937
pub trait NullifierWrite: StateWrite {
4038
/// Sets the LQT nullifier in the NV storage.
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,59 @@ pub mod lqt {
1212
format!("funding/lqt/v1/nullifier/{epoch_index:020}/lookup/{nullifier}")
1313
}
1414
}
15+
16+
pub mod votes {
17+
use penumbra_sdk_asset::asset;
18+
use penumbra_sdk_keys::{address::ADDRESS_LEN_BYTES, Address};
19+
20+
const PART0: &'static str = "funding/lqt/v1/votes/";
21+
const EPOCH_LEN: usize = 20;
22+
const PART1: &'static str = "/by_asset/";
23+
const PREFIX_LEN: usize = PART0.len() + EPOCH_LEN + PART1.len();
24+
25+
/// A prefix for accessing the votes in a given epoch, c.f. [`power_asset_address`];
26+
pub(crate) fn prefix(epoch_index: u64) -> [u8; PREFIX_LEN] {
27+
let mut bytes = [0u8; PREFIX_LEN];
28+
29+
let rest = &mut bytes;
30+
let (bytes_part0, rest) = rest.split_at_mut(PART0.len());
31+
let (bytes_epoch_index, bytes_part1) = rest.split_at_mut(EPOCH_LEN);
32+
33+
bytes_part0.copy_from_slice(PART0.as_bytes());
34+
bytes_epoch_index
35+
.copy_from_slice(format!("{epoch_index:0w$}", w = EPOCH_LEN).as_bytes());
36+
bytes_part1.copy_from_slice(PART1.as_bytes());
37+
38+
bytes
39+
}
40+
41+
const ASSET_LEN: usize = 32;
42+
const POWER_LEN: usize = 8;
43+
const RECEIPT_LEN: usize = PREFIX_LEN + ASSET_LEN + POWER_LEN + ADDRESS_LEN_BYTES;
44+
45+
/// When present, indicates that an address voted for a particular asset, with a given power.
46+
///
47+
/// To get the values ordered by descending voting power, use [`prefix`];
48+
pub(crate) fn receipt(
49+
epoch_index: u64,
50+
asset: asset::Id,
51+
power: u64,
52+
voter: &Address,
53+
) -> [u8; RECEIPT_LEN] {
54+
let mut bytes = [0u8; RECEIPT_LEN];
55+
56+
let rest = &mut bytes;
57+
let (bytes_prefix, rest) = rest.split_at_mut(PREFIX_LEN);
58+
let (bytes_asset, rest) = rest.split_at_mut(ASSET_LEN);
59+
let (bytes_power, bytes_voter) = rest.split_at_mut(POWER_LEN);
60+
61+
bytes_prefix.copy_from_slice(&prefix(epoch_index));
62+
bytes_asset.copy_from_slice(&asset.to_bytes());
63+
bytes_power.copy_from_slice(&((!power).to_be_bytes()));
64+
bytes_voter.copy_from_slice(&voter.to_vec());
65+
66+
bytes
67+
}
68+
}
1569
}
1670
}

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)