Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 @@ -634,6 +634,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 @@ -73,14 +73,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