Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/minibf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,14 @@ where
"/governance/proposals/{gov_action_id}/withdrawals",
get(routes::governance::proposal_withdrawals_by_gov_action::<D>),
)
.route(
"/governance/proposals/{tx_hash}/{cert_index}/parameters",
get(routes::governance::proposal_parameters::<D>),
)
.route(
"/governance/proposals/{gov_action_id}/parameters",
get(routes::governance::proposal_parameters_by_gov_action::<D>),
)
.with_state(facade)
.layer(
trace::TraceLayer::new_for_http()
Expand Down
55 changes: 53 additions & 2 deletions crates/minibf/src/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,65 @@ macro_rules! try_into_or_500 {
};
}

/// The `i32` a Blockfrost model types a parameter as, or a 500 when the value
/// does not fit: the chain lets a proposal set fees, sizes and counts anywhere
/// in `u64`, and a wrapped number would read as a valid, negative parameter.
pub fn i32_or_500<T>(value: T) -> Result<i32, StatusCode>
where
T: TryInto<i32> + Copy + std::fmt::Debug,
{
value.try_into().map_err(|_| {
tracing::error!(value = ?value, "parameter does not fit the i32 the model types it as");
StatusCode::INTERNAL_SERVER_ERROR
})
}

pub fn round_f64<const DECIMALS: u8>(val: f64) -> f64 {
let multiplier = 10_f64.powi(DECIMALS as i32);
(val * multiplier).round() / multiplier
}

/// A ratio as the plain quotient, at the precision `f64` gives it.
pub fn rational_to_f64_unrounded(val: &alonzo::RationalNumber) -> f64 {
val.numerator as f64 / val.denominator as f64
}

/// A ratio rounded to `DECIMALS` places.
pub fn rational_to_f64<const DECIMALS: u8>(val: &alonzo::RationalNumber) -> f64 {
let res = val.numerator as f64 / val.denominator as f64;
round_f64::<DECIMALS>(res)
round_f64::<DECIMALS>(rational_to_f64_unrounded(val))
}

/// How a protocol parameter model writes its ratios out.
///
/// Blockfrost rounds the parameters in force to the places it shows them
/// with, but serves the change a proposal asks for straight from db-sync's
/// `param_proposal` columns at full `double precision`: a proposal setting
/// tau to 1/6 comes back as 0.16666666666666666 where
/// `/epochs/{n}/parameters` says 0.167. The models that share their field
/// mapping through `protocol_params_model!` name the format as a type, so
/// the places stay written at the field and only whether they apply varies.
pub trait RatioFormat {
/// `value` as served, `DECIMALS` being the places the field is shown
/// with whenever it is rounded at all.
fn to_f64<const DECIMALS: u8>(value: &alonzo::RationalNumber) -> f64;
}

/// Rounded to the places written at the field.
pub struct Rounded;

impl RatioFormat for Rounded {
fn to_f64<const DECIMALS: u8>(value: &alonzo::RationalNumber) -> f64 {
rational_to_f64::<DECIMALS>(value)
}
}

/// The quotient as is, whatever places the field writes.
pub struct Unrounded;

impl RatioFormat for Unrounded {
fn to_f64<const DECIMALS: u8>(value: &alonzo::RationalNumber) -> f64 {
rational_to_f64_unrounded(value)
}
}

const DREP_HRP: bech32::Hrp = bech32::Hrp::parse_unchecked("drep");
Expand Down
263 changes: 155 additions & 108 deletions crates/minibf/src/routes/epochs/mapping.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;

use crate::{
mapping::{rational_to_f64, IntoModel},
mapping::{rational_to_f64, IntoModel, Rounded},
routes::epochs::cost_models::get_named_cost_model,
};
use blockfrost_openapi::models::{
Expand All @@ -24,7 +24,11 @@ fn cost_models_to_key_value(cost_models: &CostModels) -> Vec<(&'static str, &[i6
.collect()
}

fn map_cost_models_raw(
/// Cost models as the raw operation-cost vectors the chain carries.
///
/// `/governance/proposals/…/parameters` returns exactly this, while the epoch
/// endpoints run the vectors through [`map_cost_models_named`] first.
pub(crate) fn map_cost_models_raw(
cost_models: &CostModels,
) -> Option<Option<HashMap<String, serde_json::Value>>> {
let as_vec = cost_models_to_key_value(cost_models);
Expand Down Expand Up @@ -67,133 +71,176 @@ fn map_cost_models_named(cost_models: &CostModels) -> Option<HashMap<String, ser
}
}

pub struct ParametersModelBuilder<'a> {
pub epoch: Epoch,
pub params: PParamsSet,
pub genesis: &'a Genesis,
pub nonce: Option<String>,
}

impl<'a> IntoModel<EpochParamContent> for ParametersModelBuilder<'a> {
type SortKey = ();

fn into_model(self) -> Result<EpochParamContent, axum::http::StatusCode> {
let Self {
genesis,
epoch,
params,
nonce,
} = self;
/// A protocol parameter model built off a [`PParamsSet`], with the fields
/// every Blockfrost parameter model renders the same way filled in.
///
/// `/epochs/{n}/parameters` and `/governance/proposals/…/parameters` share
/// thirty-odd nullable fields that read straight off the set and differ in
/// the rest: the epoch model reports the values in force, so it types most
/// of its fields as plain values and falls back to genesis, while the
/// proposal model reports a change, so every field of it is nullable. The
/// caller passes the set, the [`RatioFormat`](crate::mapping::RatioFormat)
/// to write ratios with and the model literal holding the fields the model
/// owns, and the shared ones are added to it. The literal stays exhaustive,
/// so a field the model grows, or one named on both sides, is a compile
/// error rather than a silent default.
///
/// The expansion uses `?`, so it belongs in a function returning
/// `Result<_, StatusCode>`: the models type counts and sizes as `i32` while a
/// proposal can set them anywhere in the chain's range, and a value past
/// `i32::MAX` is a 500 rather than a parameter wrapped negative.
macro_rules! protocol_params_model {
($params:ident, $ratio:ident, $model:ident { $($field:ident: $value:expr),* $(,)? }) => {{
use $crate::mapping::RatioFormat as _;

let out = EpochParamContent {
epoch: epoch as i32,
a0: rational_to_f64::<3>(&genesis.shelley.protocol_params.a0),
e_max: genesis.shelley.protocol_params.e_max as i32,
max_tx_size: params.max_transaction_size_or_default() as i32,
max_block_size: params.max_block_body_size_or_default() as i32,
max_block_header_size: params.max_block_header_size_or_default() as i32,
min_fee_a: params.min_fee_a_or_default() as i32,
min_fee_b: params.min_fee_b_or_default() as i32,
min_utxo: params
.ada_per_utxo_byte()
.unwrap_or(genesis.shelley.protocol_params.min_utxo_value)
.to_string(),
coins_per_utxo_size: params.ada_per_utxo_byte().map(|x| x.to_string()),
coins_per_utxo_word: params.ada_per_utxo_byte().map(|x| x.to_string()),
key_deposit: params.key_deposit_or_default().to_string(),
pool_deposit: params.pool_deposit_or_default().to_string(),
n_opt: params.desired_number_of_stake_pools_or_default() as i32,
rho: params
.rho()
.map(|x| rational_to_f64::<3>(&x))
.unwrap_or_default(),
tau: params
.tau()
.map(|x| rational_to_f64::<3>(&x))
.unwrap_or_default(),
min_pool_cost: params.min_pool_cost_or_default().to_string(),
protocol_major_ver: params.protocol_major().unwrap_or_default() as i32,
protocol_minor_ver: params.protocol_version_or_default().1 as i32,
max_val_size: params.max_value_size().map(|x| x.to_string()),
collateral_percent: params.collateral_percentage().map(|x| x as i32),
max_collateral_inputs: params.max_collateral_inputs().map(|x| x as i32),
price_mem: params
$model {
$($field: $value,)*
max_val_size: $params.max_value_size().map(|x| x.to_string()),
collateral_percent: $params
.collateral_percentage()
.map($crate::mapping::i32_or_500)
.transpose()?,
max_collateral_inputs: $params
.max_collateral_inputs()
.map($crate::mapping::i32_or_500)
.transpose()?,
// One db-sync column, under both of the names Blockfrost gives it.
coins_per_utxo_size: $params.ada_per_utxo_byte().map(|x| x.to_string()),
coins_per_utxo_word: $params.ada_per_utxo_byte().map(|x| x.to_string()),
price_mem: $params
.execution_costs()
.map(|x| rational_to_f64::<4>(&x.mem_price)),
price_step: params
.map(|x| $ratio::to_f64::<4>(&x.mem_price)),
price_step: $params
.execution_costs()
.map(|x| rational_to_f64::<9>(&x.step_price)),
max_tx_ex_mem: params.max_tx_ex_units().map(|x| x.mem.to_string()),
max_tx_ex_steps: params.max_tx_ex_units().map(|x| x.steps.to_string()),
max_block_ex_mem: params.max_block_ex_units().map(|x| x.mem.to_string()),
max_block_ex_steps: params.max_block_ex_units().map(|x| x.steps.to_string()),
min_fee_ref_script_cost_per_byte: params
.map(|x| $ratio::to_f64::<9>(&x.step_price)),
max_tx_ex_mem: $params.max_tx_ex_units().map(|x| x.mem.to_string()),
max_tx_ex_steps: $params.max_tx_ex_units().map(|x| x.steps.to_string()),
max_block_ex_mem: $params.max_block_ex_units().map(|x| x.mem.to_string()),
max_block_ex_steps: $params.max_block_ex_units().map(|x| x.steps.to_string()),
min_fee_ref_script_cost_per_byte: $params
.min_fee_ref_script_cost_per_byte()
.map(|x| rational_to_f64::<3>(&x)),
drep_deposit: params.drep_deposit().map(|x| x.to_string()),
drep_activity: params.drep_inactivity_period().map(|x| x.to_string()),
cost_models_raw: map_cost_models_raw(&params.cost_models_for_script_languages()),
cost_models: map_cost_models_named(&params.cost_models_for_script_languages()),
pvt_motion_no_confidence: params
.map(|x| $ratio::to_f64::<3>(&x)),
drep_deposit: $params.drep_deposit().map(|x| x.to_string()),
drep_activity: $params.drep_inactivity_period().map(|x| x.to_string()),
pvt_motion_no_confidence: $params
.pool_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.motion_no_confidence)),
pvt_committee_normal: params
.map(|x| $ratio::to_f64::<3>(&x.motion_no_confidence)),
pvt_committee_normal: $params
.pool_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.committee_normal)),
pvt_committee_no_confidence: params
.map(|x| $ratio::to_f64::<3>(&x.committee_normal)),
pvt_committee_no_confidence: $params
.pool_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.committee_no_confidence)),
pvt_hard_fork_initiation: params
.map(|x| $ratio::to_f64::<3>(&x.committee_no_confidence)),
pvt_hard_fork_initiation: $params
.pool_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.hard_fork_initiation)),
dvt_motion_no_confidence: params
.map(|x| $ratio::to_f64::<3>(&x.hard_fork_initiation)),
// The other column Blockfrost renders under two names.
pvtpp_security_group: $params
.pool_voting_thresholds()
.map(|x| $ratio::to_f64::<3>(&x.security_voting_threshold)),
pvt_p_p_security_group: $params
.pool_voting_thresholds()
.map(|x| $ratio::to_f64::<3>(&x.security_voting_threshold)),
dvt_motion_no_confidence: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.motion_no_confidence)),
dvt_committee_normal: params
.map(|x| $ratio::to_f64::<3>(&x.motion_no_confidence)),
dvt_committee_normal: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.committee_normal)),
dvt_committee_no_confidence: params
.map(|x| $ratio::to_f64::<3>(&x.committee_normal)),
dvt_committee_no_confidence: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.committee_no_confidence)),
dvt_update_to_constitution: params
.map(|x| $ratio::to_f64::<3>(&x.committee_no_confidence)),
dvt_update_to_constitution: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.update_constitution)),
dvt_hard_fork_initiation: params
.map(|x| $ratio::to_f64::<3>(&x.update_constitution)),
dvt_hard_fork_initiation: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.hard_fork_initiation)),
dvt_p_p_network_group: params
.map(|x| $ratio::to_f64::<3>(&x.hard_fork_initiation)),
dvt_p_p_network_group: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.pp_network_group)),
dvt_p_p_economic_group: params
.map(|x| $ratio::to_f64::<3>(&x.pp_network_group)),
dvt_p_p_economic_group: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.pp_economic_group)),
dvt_p_p_technical_group: params
.map(|x| $ratio::to_f64::<3>(&x.pp_economic_group)),
dvt_p_p_technical_group: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.pp_technical_group)),
dvt_p_p_gov_group: params
.map(|x| $ratio::to_f64::<3>(&x.pp_technical_group)),
dvt_p_p_gov_group: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.pp_governance_group)),
dvt_treasury_withdrawal: params
.map(|x| $ratio::to_f64::<3>(&x.pp_governance_group)),
dvt_treasury_withdrawal: $params
.drep_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.treasury_withdrawal)),
committee_min_size: params.min_committee_size().map(|x| x.to_string()),
committee_max_term_length: params.committee_term_limit().map(|x| x.to_string()),
gov_action_lifetime: params
.map(|x| $ratio::to_f64::<3>(&x.treasury_withdrawal)),
committee_min_size: $params.min_committee_size().map(|x| x.to_string()),
committee_max_term_length: $params.committee_term_limit().map(|x| x.to_string()),
gov_action_lifetime: $params
.governance_action_validity_period()
.map(|x| x.to_string()),
gov_action_deposit: params.governance_action_deposit().map(|x| x.to_string()),
pvtpp_security_group: params
.pool_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.security_voting_threshold)),
pvt_p_p_security_group: params
.pool_voting_thresholds()
.map(|x| rational_to_f64::<3>(&x.security_voting_threshold)),
nonce: nonce.unwrap_or_default(),
gov_action_deposit: $params.governance_action_deposit().map(|x| x.to_string()),
// Babbage retired it, so neither model has a value to show.
extra_entropy: None,
decentralisation_param: rational_to_f64::<3>(
&params.decentralization_constant_or_default(),
),
};
}
}};
}

pub(crate) use protocol_params_model;

pub struct ParametersModelBuilder<'a> {
pub epoch: Epoch,
pub params: PParamsSet,
pub genesis: &'a Genesis,
pub nonce: Option<String>,
}

impl<'a> IntoModel<EpochParamContent> for ParametersModelBuilder<'a> {
type SortKey = ();

fn into_model(self) -> Result<EpochParamContent, axum::http::StatusCode> {
let Self {
genesis,
epoch,
params,
nonce,
} = self;

let out = protocol_params_model!(
params,
Rounded,
EpochParamContent {
epoch: epoch as i32,
a0: rational_to_f64::<3>(&genesis.shelley.protocol_params.a0),
e_max: genesis.shelley.protocol_params.e_max as i32,
max_tx_size: params.max_transaction_size_or_default() as i32,
max_block_size: params.max_block_body_size_or_default() as i32,
max_block_header_size: params.max_block_header_size_or_default() as i32,
min_fee_a: params.min_fee_a_or_default() as i32,
min_fee_b: params.min_fee_b_or_default() as i32,
min_utxo: params
.ada_per_utxo_byte()
.unwrap_or(genesis.shelley.protocol_params.min_utxo_value)
.to_string(),
key_deposit: params.key_deposit_or_default().to_string(),
pool_deposit: params.pool_deposit_or_default().to_string(),
n_opt: params.desired_number_of_stake_pools_or_default() as i32,
rho: params
.rho()
.map(|x| rational_to_f64::<3>(&x))
.unwrap_or_default(),
tau: params
.tau()
.map(|x| rational_to_f64::<3>(&x))
.unwrap_or_default(),
min_pool_cost: params.min_pool_cost_or_default().to_string(),
protocol_major_ver: params.protocol_major().unwrap_or_default() as i32,
protocol_minor_ver: params.protocol_version_or_default().1 as i32,
cost_models_raw: map_cost_models_raw(&params.cost_models_for_script_languages()),
cost_models: map_cost_models_named(&params.cost_models_for_script_languages()),
nonce: nonce.unwrap_or_default(),
decentralisation_param: rational_to_f64::<3>(
&params.decentralization_constant_or_default(),
),
}
);

Ok(out)
}
Expand Down
Loading
Loading