Skip to content

Commit

Permalink
staking: use block height to enforce unbonding delay (#3923)
Browse files Browse the repository at this point in the history
Close #3738, this PR makes the unbonding mechanism based on block delay
rather than epochs. In practice, this means that a user will wait X
blocks for a validator pool to unbond (rather than Y epochs).

To achieve this, this PR:
- defines `StakeParamater::unbonding_delay` measured in blocks
- parameterize unbonding tokens based on a `start_height`
- deprecate `start_epoch_index` fields in delegate/claim actions
- strip `epoch_index` from validator `RateData`s
- generalize the unbonding token denom format: 
`2.013718unbonding_start_at_1540_penumbravalid1` (vs. `_epoch_XXX`)

Important point about the mechanism: we bind tokens to the starting
height of the epoch that they belong to. This let us avoid binding
transactions to specific block heights.
  • Loading branch information
erwanor authored Mar 12, 2024
1 parent ec9837a commit b429bc7
Show file tree
Hide file tree
Showing 35 changed files with 543 additions and 243 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

37 changes: 27 additions & 10 deletions crates/bin/pcli/src/command/query/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::{anyhow, Context, Result};
use comfy_table::{presets, Table};
use futures::TryStreamExt;
use penumbra_app::params::AppParameters;
use penumbra_num::fixpoint::U128x128;
use penumbra_proto::{
core::app::v1::{
query_service_client::QueryServiceClient as AppQueryServiceClient, AppParametersRequest,
Expand All @@ -16,7 +17,7 @@ use penumbra_proto::{
tendermint_proxy_service_client::TendermintProxyServiceClient, GetStatusRequest,
},
};
use penumbra_stake::validator;
use penumbra_stake::{validator, BPS_SQUARED_SCALING_FACTOR};

// TODO: remove this subcommand and merge into `pcli q`

Expand Down Expand Up @@ -56,35 +57,51 @@ impl ChainCmd {
.ok_or_else(|| anyhow::anyhow!("empty AppParametersResponse message"))?
.try_into()?;

fn scale_rate(rate_bps_sq: u64) -> U128x128 {
let rate_bps_sq = U128x128::from(rate_bps_sq);
(rate_bps_sq / *BPS_SQUARED_SCALING_FACTOR).expect("non zero denominator")
}

fn display_rate_percent(rate_bps_sq: u64) -> String {
let rate = scale_rate(rate_bps_sq);
let hundred = U128x128::from(100u128);
let rate_pct: U128x128 = (rate * hundred).expect("rate is around 1");
format!("{}%", rate_pct)
}

println!("Chain Parameters:");
let mut table = Table::new();
table.load_preset(presets::NOTHING);
table
.set_header(vec!["", ""])
.add_row(vec!["Chain ID", &params.chain_id])
.add_row(vec![
"Epoch Duration",
"Epoch Duration (# of blocks)",
&format!("{}", params.sct_params.epoch_duration),
])
.add_row(vec![
"Unbonding Epochs",
&format!("{}", params.stake_params.unbonding_epochs),
"Unbonding delay (# of blocks)",
&format!("{}", params.stake_params.unbonding_delay),
])
.add_row(vec![
"Minimum Validator Stake (upenumbra)",
&format!("{}", params.stake_params.min_validator_stake),
])
.add_row(vec![
"Active Validator Limit",
&format!("{}", params.stake_params.active_validator_limit),
])
.add_row(vec![
"Base Reward Rate (bps^2)",
&format!("{}", params.stake_params.base_reward_rate),
"Base Reward Rate",
&display_rate_percent(params.stake_params.base_reward_rate),
])
.add_row(vec![
"Slashing Penalty (Misbehavior) (bps^2)",
&format!("{}", params.stake_params.slashing_penalty_misbehavior),
"Slashing Penalty (Misbehavior)",
&display_rate_percent(params.stake_params.slashing_penalty_misbehavior),
])
.add_row(vec![
"Slashing Penalty (Downtime) (bps^2)",
&format!("{}", params.stake_params.slashing_penalty_downtime),
"Slashing Penalty (Downtime)",
&display_rate_percent(params.stake_params.slashing_penalty_downtime),
])
.add_row(vec![
"Signed Blocks Window (blocks)",
Expand Down
57 changes: 46 additions & 11 deletions crates/bin/pcli/src/command/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,19 +546,31 @@ impl TxCmd {

let to = to.parse::<IdentityKey>()?;

let mut client = StakeQueryServiceClient::new(app.pd_channel().await?);
let rate_data: RateData = client
let mut stake_client = StakeQueryServiceClient::new(app.pd_channel().await?);
let rate_data: RateData = stake_client
.current_validator_rate(tonic::Request::new(to.into()))
.await?
.into_inner()
.try_into()?;

let mut sct_client = SctQueryServiceClient::new(app.pd_channel().await?);
let latest_sync_height = app.view().status().await?.full_sync_height;
let epoch = sct_client
.epoch_by_height(EpochByHeightRequest {
height: latest_sync_height,
})
.await?
.into_inner()
.epoch
.expect("epoch must be available")
.into();

let mut planner = Planner::new(OsRng);
planner
.set_gas_prices(gas_prices)
.set_fee_tier((*fee_tier).into());
let plan = planner
.delegate(unbonded_amount, rate_data)
.delegate(epoch, unbonded_amount, rate_data)
.plan(app.view(), AddressIndex::new(*source))
.await
.context("can't plan delegation")?;
Expand Down Expand Up @@ -588,20 +600,32 @@ impl TxCmd {

let from = delegation_token.validator();

let mut client = StakeQueryServiceClient::new(app.pd_channel().await?);
let rate_data: RateData = client
let mut stake_client = StakeQueryServiceClient::new(app.pd_channel().await?);
let rate_data: RateData = stake_client
.current_validator_rate(tonic::Request::new(from.into()))
.await?
.into_inner()
.try_into()?;

let mut sct_client = SctQueryServiceClient::new(app.pd_channel().await?);
let latest_sync_height = app.view().status().await?.full_sync_height;
let epoch = sct_client
.epoch_by_height(EpochByHeightRequest {
height: latest_sync_height,
})
.await?
.into_inner()
.epoch
.expect("epoch must be available")
.into();

let mut planner = Planner::new(OsRng);
planner
.set_gas_prices(gas_prices)
.set_fee_tier((*fee_tier).into());

let plan = planner
.undelegate(delegation_value.amount, rate_data)
.undelegate(epoch, delegation_value.amount, rate_data)
.plan(
app.view
.as_mut()
Expand Down Expand Up @@ -651,15 +675,26 @@ impl TxCmd {
})
{
println!("claiming {}", token.denom().default_unit());

let validator_identity = token.validator();
let start_epoch_index = token.start_epoch_index();
let unbonding_start_height = token.unbonding_start_height();
let end_epoch_index = current_epoch.index;

let mut client = StakeQueryServiceClient::new(channel.clone());
let penalty: Penalty = client
let mut sct_client = SctQueryServiceClient::new(channel.clone());
let epoch_start = sct_client
.epoch_by_height(EpochByHeightRequest {
height: unbonding_start_height,
})
.await?
.into_inner()
.epoch
.context("unable to get epoch for unbonding start height")?;

let mut stake_client = StakeQueryServiceClient::new(channel.clone());
let penalty: Penalty = stake_client
.validator_penalty(tonic::Request::new(ValidatorPenaltyRequest {
identity_key: Some(validator_identity.into()),
start_epoch_index,
start_epoch_index: epoch_start.index,
end_epoch_index,
}))
.await?
Expand All @@ -685,7 +720,7 @@ impl TxCmd {
let plan = planner
.undelegate_claim(UndelegateClaimPlan {
validator_identity,
start_epoch_index,
unbonding_start_height,
penalty,
unbonding_amount,
balance_blinding: Fr::rand(&mut OsRng),
Expand Down
4 changes: 2 additions & 2 deletions crates/bin/pcli/tests/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,8 @@ fn undelegate_claim_parameters_vs_current_undelegate_claim_circuit() {
let validator_identity = IdentityKey((&sk).into());
let unbonding_amount = Amount::from(value1_amount);

let start_epoch_index = 1;
let unbonding_token = UnbondingToken::new(validator_identity, start_epoch_index);
let start_height = 1;
let unbonding_token = UnbondingToken::new(validator_identity, start_height);
let unbonding_id = unbonding_token.id();
let penalty = Penalty::from_bps_squared(penalty_amount);
let balance = penalty.balance_for_claim(unbonding_id, unbonding_amount);
Expand Down
6 changes: 3 additions & 3 deletions crates/bin/pd/src/testnet/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ impl TestnetConfig {
validators: Vec<Validator>,
active_validator_limit: Option<u64>,
epoch_duration: Option<u64>,
unbonding_epochs: Option<u64>,
unbonding_delay: Option<u64>,
proposal_voting_blocks: Option<u64>,
) -> anyhow::Result<penumbra_genesis::Content> {
let default_gov_params = penumbra_governance::params::GovernanceParameters::default();
Expand All @@ -205,8 +205,8 @@ impl TestnetConfig {
stake_params: StakeParameters {
active_validator_limit: active_validator_limit
.unwrap_or(default_app_params.stake_params.active_validator_limit),
unbonding_epochs: unbonding_epochs
.unwrap_or(default_app_params.stake_params.unbonding_epochs),
unbonding_delay: unbonding_delay
.unwrap_or(default_app_params.stake_params.unbonding_delay),
..Default::default()
},
},
Expand Down
Binary file modified crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs
Binary file not shown.
8 changes: 4 additions & 4 deletions crates/core/app/src/params/change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ impl AppParameters {
},
stake_params:
StakeParameters {
unbonding_epochs: _,
active_validator_limit,
base_reward_rate: _,
slashing_penalty_misbehavior: _,
slashing_penalty_downtime: _,
signed_blocks_window_len,
missed_blocks_maximum: _,
min_validator_stake: _,
unbonding_delay: _,
},
// IMPORTANT: Don't use `..` here! We want to ensure every single field is verified!
} = self;
Expand Down Expand Up @@ -148,14 +148,14 @@ impl AppParameters {
},
stake_params:
StakeParameters {
unbonding_epochs,
active_validator_limit,
base_reward_rate,
slashing_penalty_misbehavior,
slashing_penalty_downtime,
signed_blocks_window_len,
missed_blocks_maximum,
min_validator_stake,
unbonding_delay,
},
// IMPORTANT: Don't use `..` here! We want to ensure every single field is verified!
} = self;
Expand All @@ -167,8 +167,8 @@ impl AppParameters {
"epoch duration must be at least one block",
),
(
*unbonding_epochs >= 1,
"unbonding must take at least one epoch",
*unbonding_delay >= epoch_duration * 2 + 1,
"unbonding must take at least two epochs",
),
(
*active_validator_limit > 3,
Expand Down
6 changes: 3 additions & 3 deletions crates/core/asset/src/asset/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,10 +356,10 @@ pub static REGISTRY: Lazy<Registry> = Lazy::new(|| {
// Note: this regex must be in sync with UnbondingToken::try_from
// and VALIDATOR_IDENTITY_BECH32_PREFIX in the penumbra-stake crate
// TODO: this doesn't restrict the length of the bech32 encoding
"^uunbonding_(?P<data>epoch_(?P<start>[0-9]+)_until_(?P<end>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
"^uunbonding_(?P<data>start_at_(?P<start>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
&[
"^unbonding_(?P<data>epoch_(?P<start>[0-9]+)_until_(?P<end>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
"^munbonding_(?P<data>epoch_(?P<start>[0-9]+)_until_(?P<end>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
"^unbonding_(?P<data>start_at_(?P<start>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
"^munbonding_(?P<data>start_at_(?P<start>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
],
(|data: &str| {
assert!(!data.is_empty());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@ pub trait StateWriteExt: StateWrite + StateReadExt {

/// Writes the current FMD parameters to the JMT.
fn put_current_fmd_parameters(&mut self, params: fmd::Parameters) {
// MERGEBLOCK(erwan): read through the update mechanism for FMD params
// do we need to flag that shielded pool params were updated?
self.put(fmd::state_key::parameters::current().into(), params)
}

Expand Down
Loading

0 comments on commit b429bc7

Please sign in to comment.