Skip to content

Commit

Permalink
Implement check and execute for LQT votes (#5033)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cronokirby authored and conorsch committed Feb 5, 2025
1 parent 3eb2418 commit 2ee34d6
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 10 deletions.
1 change: 1 addition & 0 deletions crates/core/component/funding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ component = [
"penumbra-sdk-proto/cnidarium",
"penumbra-sdk-community-pool/component",
"penumbra-sdk-distributions/component",
"penumbra-sdk-governance/component",
"penumbra-sdk-sct/component",
"penumbra-sdk-shielded-pool/component",
"penumbra-sdk-stake/component",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
use anyhow::Context as _;
use anyhow::{anyhow, Context as _};
use async_trait::async_trait;
use cnidarium::StateWrite;
use penumbra_sdk_asset::asset::Denom;
use cnidarium::{StateRead, StateWrite};
use cnidarium_component::ActionHandler;
use penumbra_sdk_asset::{asset::Denom, Value};
use penumbra_sdk_governance::StateReadExt as _;
use penumbra_sdk_num::Amount;
use penumbra_sdk_proof_params::DELEGATOR_VOTE_PROOF_VERIFICATION_KEY;
use penumbra_sdk_txhash::TransactionContext;
use penumbra_sdk_sct::component::clock::EpochRead as _;
use penumbra_sdk_sct::epoch::Epoch;
use penumbra_sdk_stake::component::validator_handler::ValidatorDataRead as _;
use penumbra_sdk_tct::Position;
use penumbra_sdk_txhash::{TransactionContext, TransactionId};

use crate::component::liquidity_tournament::{
nullifier::{NullifierRead as _, NullifierWrite as _},
votes::StateWriteExt as _,
};
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!(
Expand All @@ -26,6 +36,37 @@ fn is_valid_denom(denom: &Denom) -> anyhow::Result<()> {
Ok(())
}

// Check that the start position is early enough to vote in the current epoch.
async fn start_position_good_for_epoch(epoch: Epoch, start: Position) -> anyhow::Result<()> {
anyhow::ensure!(
epoch.index > u64::from(start.epoch()),
"position {start:?} is not before epoch {epoch:?}"
);
Ok(())
}

/// Fetch the unbonded equivalent of some purported delegation token.
///
/// Will fail if (either):
/// - the token is not for a known validator,
/// - the validator does not have any rate data.
async fn unbonded_amount(state: impl StateRead, value: Value) -> anyhow::Result<Amount> {
let validator = state.validator_by_delegation_asset(value.asset_id).await?;
let rate = state
.get_validator_rate(&validator)
.await?
.ok_or_else(|| anyhow!("{} has no rate data", &validator))?;
Ok(rate.unbonded_amount(value.amount))
}

// This isolates the logic for how we should handle out of bounds amounts.
fn voting_power(amount: Amount) -> u64 {
amount
.value()
.try_into()
.expect("someone acquired {amount:?} > u64::MAX worth of delegation tokens!")
}

#[async_trait]
impl ActionHandler for ActionLiquidityTournamentVote {
type CheckStatelessContext = TransactionContext;
Expand Down Expand Up @@ -70,7 +111,37 @@ impl ActionHandler for ActionLiquidityTournamentVote {
Ok(())
}

async fn check_and_execute<S: StateWrite>(&self, _state: S) -> anyhow::Result<()> {
todo!()
async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> anyhow::Result<()> {
// 1. Check that the start position can vote in this round.
let current_epoch = state
.get_current_epoch()
.await
.expect("failed to fetch current epoch");
start_position_good_for_epoch(current_epoch, self.body.start_position).await?;
// 2. We can tally, as long as the nullifier hasn't been used yet (this round).
let nullifier = self.body.nullifier;
let nullifier_exists = state.get_lqt_spent_nullifier(nullifier).await.is_some();
anyhow::ensure!(
!nullifier_exists,
"nullifier {} already voted in epoch {}",
self.body.nullifier,
current_epoch.index
);
state.put_lqt_spent_nullifier(current_epoch.index, nullifier, TransactionId([0u8; 32]));
// 3. Ok, actually tally.
let power = voting_power(unbonded_amount(&state, self.body.value).await?);
let incentivized = self
.body
.incentivized_id()
.ok_or_else(|| anyhow!("{:?} is not a base denom", self.body.incentivized))?;
state
.tally(
current_epoch.index,
incentivized,
power,
&self.body.rewards_recipient,
)
.await?;
Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod nullifier;
pub mod votes;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use cnidarium::{StateRead, StateWrite};
use penumbra_sdk_proto::{StateReadProto, StateWriteProto};
use penumbra_sdk_sct::{component::clock::EpochRead, Nullifier};

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

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

#[allow(dead_code)]
#[async_trait]
pub trait NullifierWrite: StateWrite {
/// Sets the LQT nullifier in the NV storage.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use async_trait::async_trait;
use cnidarium::StateWrite;
use penumbra_sdk_asset::asset;
use penumbra_sdk_keys::Address;

use crate::component::state_key;

#[async_trait]
pub trait StateWriteExt: StateWrite {
// Keeping this as returning a result to not have to touch other code if it changes to return an error.
async fn tally(
&mut self,
epoch: u64,
asset: asset::Id,
power: u64,
voter: &Address,
) -> anyhow::Result<()> {
self.nonverifiable_put_raw(
state_key::lqt::v1::votes::receipt(epoch, asset, power, voter).to_vec(),
Vec::default(),
);
Ok(())
}
}

impl<T: StateWrite + ?Sized> StateWriteExt for T {}
54 changes: 54 additions & 0 deletions crates/core/component/funding/src/component/state_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,59 @@ pub mod lqt {
format!("funding/lqt/v1/nullifier/{epoch_index:020}/lookup/{nullifier}")
}
}

pub mod votes {
use penumbra_sdk_asset::asset;
use penumbra_sdk_keys::{address::ADDRESS_LEN_BYTES, Address};

const PART0: &'static str = "funding/lqt/v1/votes/";
const EPOCH_LEN: usize = 20;
const PART1: &'static str = "/by_asset/";
const PREFIX_LEN: usize = PART0.len() + EPOCH_LEN + PART1.len();

/// A prefix for accessing the votes in a given epoch, c.f. [`power_asset_address`];
pub(crate) fn prefix(epoch_index: u64) -> [u8; PREFIX_LEN] {
let mut bytes = [0u8; PREFIX_LEN];

let rest = &mut bytes;
let (bytes_part0, rest) = rest.split_at_mut(PART0.len());
let (bytes_epoch_index, bytes_part1) = rest.split_at_mut(EPOCH_LEN);

bytes_part0.copy_from_slice(PART0.as_bytes());
bytes_epoch_index
.copy_from_slice(format!("{epoch_index:0w$}", w = EPOCH_LEN).as_bytes());
bytes_part1.copy_from_slice(PART1.as_bytes());

bytes
}

const ASSET_LEN: usize = 32;
const POWER_LEN: usize = 8;
const RECEIPT_LEN: usize = PREFIX_LEN + ASSET_LEN + POWER_LEN + ADDRESS_LEN_BYTES;

/// When present, indicates that an address voted for a particular asset, with a given power.
///
/// To get the values ordered by descending voting power, use [`prefix`];
pub(crate) fn receipt(
epoch_index: u64,
asset: asset::Id,
power: u64,
voter: &Address,
) -> [u8; RECEIPT_LEN] {
let mut bytes = [0u8; RECEIPT_LEN];

let rest = &mut bytes;
let (bytes_prefix, rest) = rest.split_at_mut(PREFIX_LEN);
let (bytes_asset, rest) = rest.split_at_mut(ASSET_LEN);
let (bytes_power, bytes_voter) = rest.split_at_mut(POWER_LEN);

bytes_prefix.copy_from_slice(&prefix(epoch_index));
bytes_asset.copy_from_slice(&asset.to_bytes());
bytes_power.copy_from_slice(&((!power).to_be_bytes()));
bytes_voter.copy_from_slice(&voter.to_vec());

bytes
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use anyhow::{anyhow, Context};
use decaf377_rdsa::{Signature, SpendAuth, VerificationKey};
use penumbra_sdk_asset::{asset::Denom, balance, Value};
use penumbra_sdk_asset::{
asset::{self, Denom, REGISTRY},
balance, Value,
};
use penumbra_sdk_keys::Address;
use penumbra_sdk_proto::{core::component::funding::v1 as pb, DomainType};
use penumbra_sdk_sct::Nullifier;
Expand Down Expand Up @@ -34,6 +37,17 @@ pub struct LiquidityTournamentVoteBody {
pub rk: VerificationKey<SpendAuth>,
}

impl LiquidityTournamentVoteBody {
/// Get the asset id that should be incentivized.
///
/// This will return None if the denom is not a base denom.
pub fn incentivized_id(&self) -> Option<asset::Id> {
REGISTRY
.parse_denom(&self.incentivized.denom)
.map(|x| x.id())
}
}

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

0 comments on commit 2ee34d6

Please sign in to comment.