diff --git a/pallas-configs/src/byron.rs b/pallas-configs/src/byron.rs index 1391851f5..17a691509 100644 --- a/pallas-configs/src/byron.rs +++ b/pallas-configs/src/byron.rs @@ -80,6 +80,7 @@ pub type BootStakeWeight = u16; #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct HeavyDelegation { + pub omega: u32, pub issuer_pk: String, pub delegate_pk: String, pub cert: String, diff --git a/pallas-traverse/src/wellknown.rs b/pallas-traverse/src/wellknown.rs index aedfbfd99..c96140f84 100644 --- a/pallas-traverse/src/wellknown.rs +++ b/pallas-traverse/src/wellknown.rs @@ -31,6 +31,18 @@ pub struct GenesisValues { pub shelley_known_slot: u64, pub shelley_known_hash: String, pub shelley_known_time: u64, + + // Hard Fork Combinator (HFC) era transition slots + #[serde(default)] + pub allegra_start_slot: Option, + #[serde(default)] + pub mary_start_slot: Option, + #[serde(default)] + pub alonzo_start_slot: Option, + #[serde(default)] + pub babbage_start_slot: Option, + #[serde(default)] + pub conway_start_slot: Option, } impl GenesisValues { @@ -51,6 +63,13 @@ impl GenesisValues { shelley_known_hash: "aa83acbf5904c0edfe4d79b3689d3d00fcfc553cf360fd2229b98d464c28e9de" .to_string(), shelley_known_time: 1596059091, + + // Era transitions - source: official Cardano documentation + allegra_start_slot: Some(16588800), + mary_start_slot: Some(23068800), + alonzo_start_slot: Some(39916975), + babbage_start_slot: Some(72316896), + conway_start_slot: Some(133660855), } } @@ -71,6 +90,13 @@ impl GenesisValues { shelley_known_hash: "02b1c561715da9e540411123a6135ee319b02f60b9a11a603d3305556c04329f" .to_string(), shelley_known_time: 1595967616, + + // Legacy testnet era transitions - not documented + allegra_start_slot: None, + mary_start_slot: None, + alonzo_start_slot: None, + babbage_start_slot: None, + conway_start_slot: None, } } @@ -89,6 +115,13 @@ impl GenesisValues { shelley_known_hash: "268ae601af8f9214804735910a3301881fbe0eec9936db7d1fb9fc39e93d1e37" .to_string(), shelley_known_time: 1666656000, + + // Preview likely started in Babbage era + allegra_start_slot: None, + mary_start_slot: None, + alonzo_start_slot: None, + babbage_start_slot: None, + conway_start_slot: Some(31424418), } } @@ -109,6 +142,13 @@ impl GenesisValues { shelley_known_hash: "c971bfb21d2732457f9febf79d9b02b20b9a3bef12c561a78b818bcb8b35a574" .to_string(), shelley_known_time: 1655769600, + + // Preprod era transitions - not documented + allegra_start_slot: None, + mary_start_slot: None, + alonzo_start_slot: None, + babbage_start_slot: None, + conway_start_slot: None, } } @@ -123,6 +163,111 @@ impl GenesisValues { _ => None, } } + + /// Get era start slot using HFC data + pub fn era_start_slot(&self, era: crate::Era) -> Option { + use crate::Era; + match era { + Era::Byron => Some(0), + Era::Shelley => Some(self.shelley_known_slot), + Era::Allegra => self.allegra_start_slot, + Era::Mary => self.mary_start_slot, + Era::Alonzo => self.alonzo_start_slot, + Era::Babbage => self.babbage_start_slot, + Era::Conway => self.conway_start_slot, + } + } + + /// Get era from slot number using HFC data + pub fn slot_to_era(&self, slot: u64) -> crate::Era { + use crate::Era; + + if slot < self.shelley_known_slot { + Era::Byron + } else if self.allegra_start_slot.map_or(true, |s| slot < s) { + Era::Shelley + } else if self.mary_start_slot.map_or(true, |s| slot < s) { + Era::Allegra + } else if self.alonzo_start_slot.map_or(true, |s| slot < s) { + Era::Mary + } else if self.babbage_start_slot.map_or(true, |s| slot < s) { + Era::Alonzo + } else if self.conway_start_slot.map_or(true, |s| slot < s) { + Era::Babbage + } else { + Era::Conway + } + } + + /// Get list of all known eras (regardless of whether slot data is available) + pub fn available_eras(&self) -> Vec { + use crate::Era; + vec![ + Era::Byron, + Era::Shelley, + Era::Allegra, + Era::Mary, + Era::Alonzo, + Era::Babbage, + Era::Conway, + ] + } + + /// Get list of eras that have slot transition data configured + pub fn eras_with_slot_data(&self) -> Vec { + use crate::Era; + let mut eras = vec![Era::Byron, Era::Shelley]; // Always have these + + if self.allegra_start_slot.is_some() { + eras.push(Era::Allegra); + } + if self.mary_start_slot.is_some() { + eras.push(Era::Mary); + } + if self.alonzo_start_slot.is_some() { + eras.push(Era::Alonzo); + } + if self.babbage_start_slot.is_some() { + eras.push(Era::Babbage); + } + if self.conway_start_slot.is_some() { + eras.push(Era::Conway); + } + + eras + } + + /// Configure era transition slots with known values + /// This allows users to provide accurate slot numbers when they have them + pub fn with_era_slots( + mut self, + allegra: Option, + mary: Option, + alonzo: Option, + babbage: Option, + conway: Option, + ) -> Self { + self.allegra_start_slot = allegra; + self.mary_start_slot = mary; + self.alonzo_start_slot = alonzo; + self.babbage_start_slot = babbage; + self.conway_start_slot = conway; + self + } + + /// Set a specific era's start slot + pub fn set_era_start_slot(&mut self, era: crate::Era, slot: Option) { + use crate::Era; + match era { + Era::Byron => {} // Byron always starts at slot 0 + Era::Shelley => {} // Shelley slot is in known_slot field + Era::Allegra => self.allegra_start_slot = slot, + Era::Mary => self.mary_start_slot = slot, + Era::Alonzo => self.alonzo_start_slot = slot, + Era::Babbage => self.babbage_start_slot = slot, + Era::Conway => self.conway_start_slot = slot, + } + } } impl Default for GenesisValues { diff --git a/pallas-utxorpc/Cargo.toml b/pallas-utxorpc/Cargo.toml index 84ea59843..2be946838 100644 --- a/pallas-utxorpc/Cargo.toml +++ b/pallas-utxorpc/Cargo.toml @@ -18,7 +18,8 @@ pallas-traverse = { version = "=1.0.0-alpha.2", path = "../pallas-traverse" } pallas-primitives = { version = "=1.0.0-alpha.2", path = "../pallas-primitives" } pallas-codec = { version = "=1.0.0-alpha.2", path = "../pallas-codec" } pallas-crypto = { version = "=1.0.0-alpha.2", path = "../pallas-crypto" } -prost-types = "0.13.1" +pallas-configs = { version = "=1.0.0-alpha.2", path = "../pallas-configs" } +prost-types = "0.14.1" pallas-validate = { version = "=1.0.0-alpha.2", path = "../pallas-validate" } [dev-dependencies] diff --git a/pallas-utxorpc/src/genesis.rs b/pallas-utxorpc/src/genesis.rs new file mode 100644 index 000000000..9a31e4fc2 --- /dev/null +++ b/pallas-utxorpc/src/genesis.rs @@ -0,0 +1,346 @@ +use pallas_configs::{alonzo, byron, conway, shelley}; +use pallas_validate::utils::MultiEraProtocolParameters; +use utxorpc_spec::utxorpc::v1alpha::cardano as u5c; + +use crate::{LedgerContext, Mapper}; + +impl Mapper { + /// Map genesis configuration to UTxO RPC Genesis type + pub fn map_genesis( + &self, + byron: &byron::GenesisFile, + shelley: &shelley::GenesisFile, + alonzo: &alonzo::GenesisFile, + conway: &conway::GenesisFile, + current_params: Option, + ) -> u5c::Genesis { + u5c::Genesis { + // Shelley data + network_magic: shelley.network_magic.unwrap_or(0), + network_id: shelley.network_id.clone().unwrap_or_default(), + epoch_length: shelley.epoch_length.unwrap_or(0), + slot_length: shelley.slot_length.unwrap_or(0), + security_param: shelley.security_param.unwrap_or(0), + system_start: shelley.system_start.clone().unwrap_or_default(), + max_lovelace_supply: shelley.max_lovelace_supply.unwrap_or(0), + max_kes_evolutions: shelley.max_kes_evolutions.unwrap_or(0), + slots_per_kes_period: shelley.slots_per_kes_period.unwrap_or(0), + update_quorum: shelley.update_quorum.unwrap_or(0), + initial_funds: shelley.initial_funds.clone().unwrap_or_default(), + + // Alonzo data + lovelace_per_utxo_word: alonzo.lovelace_per_utxo_word, + max_value_size: alonzo.max_value_size, + collateral_percentage: alonzo.collateral_percentage, + max_collateral_inputs: alonzo.max_collateral_inputs, + + // Conway data + committee_min_size: conway.committee_min_size as u64, + committee_max_term_length: conway.committee_max_term_length as u64, + gov_action_lifetime: conway.gov_action_lifetime as u64, + gov_action_deposit: conway.gov_action_deposit, + drep_deposit: conway.d_rep_deposit, + drep_activity: conway.d_rep_activity as u64, + + // Byron data + start_time: byron.start_time, + boot_stakeholders: byron + .boot_stakeholders + .iter() + .map(|(k, v)| (k.clone(), *v as u64)) + .collect(), + avvm_distr: byron.avvm_distr.clone(), + non_avvm_balances: byron.non_avvm_balances.clone(), + + // Map complex nested structures + active_slots_coeff: shelley.active_slots_coeff.map(|coeff| u5c::RationalNumber { + numerator: (coeff * 1000.0) as i32, + denominator: 1000, + }), + + gen_delegs: shelley + .gen_delegs + .as_ref() + .map(|delegs| { + delegs + .iter() + .map(|(k, v)| { + ( + k.clone(), + u5c::GenDelegs { + delegate: v.delegate.clone().unwrap_or_default(), + vrf: v.vrf.clone().unwrap_or_default(), + }, + ) + }) + .collect() + }) + .unwrap_or_default(), + + // Alonzo execution prices + execution_prices: Some(u5c::ExPrices { + steps: Some(u5c::RationalNumber { + numerator: alonzo.execution_prices.pr_steps.numerator as i32, + denominator: alonzo.execution_prices.pr_steps.denominator as u32, + }), + memory: Some(u5c::RationalNumber { + numerator: alonzo.execution_prices.pr_mem.numerator as i32, + denominator: alonzo.execution_prices.pr_mem.denominator as u32, + }), + }), + + // Alonzo execution units + max_tx_ex_units: Some(u5c::ExUnits { + memory: alonzo.max_tx_ex_units.ex_units_mem, + steps: alonzo.max_tx_ex_units.ex_units_steps, + }), + max_block_ex_units: Some(u5c::ExUnits { + memory: alonzo.max_block_ex_units.ex_units_mem, + steps: alonzo.max_block_ex_units.ex_units_steps, + }), + + // Conway governance structures + committee: Some(u5c::Committee { + members: conway.committee.members.clone(), + threshold: Some(u5c::RationalNumber { + numerator: conway.committee.threshold.numerator as i32, + denominator: conway.committee.threshold.denominator as u32, + }), + }), + constitution: Some(u5c::Constitution { + anchor: Some(u5c::Anchor { + url: conway.constitution.anchor.url.clone(), + content_hash: conway.constitution.anchor.data_hash.clone().into(), + }), + hash: conway + .constitution + .script + .clone() + .unwrap_or_default() + .into(), + }), + min_fee_ref_script_cost_per_byte: Some(u5c::RationalNumber { + numerator: conway.min_fee_ref_script_cost_per_byte as i32, + denominator: 1, + }), + + // Conway voting thresholds + drep_voting_thresholds: Some(u5c::DRepVotingThresholds { + motion_no_confidence: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.motion_no_confidence * 100.0) as i32, + denominator: 100, + }), + committee_normal: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.committee_normal * 100.0) as i32, + denominator: 100, + }), + committee_no_confidence: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.committee_no_confidence * 100.0) + as i32, + denominator: 100, + }), + update_to_constitution: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.update_to_constitution * 100.0) + as i32, + denominator: 100, + }), + hard_fork_initiation: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.hard_fork_initiation * 100.0) as i32, + denominator: 100, + }), + pp_network_group: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.pp_network_group * 100.0) as i32, + denominator: 100, + }), + pp_economic_group: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.pp_economic_group * 100.0) as i32, + denominator: 100, + }), + pp_technical_group: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.pp_technical_group * 100.0) as i32, + denominator: 100, + }), + pp_gov_group: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.pp_gov_group * 100.0) as i32, + denominator: 100, + }), + treasury_withdrawal: Some(u5c::RationalNumber { + numerator: (conway.d_rep_voting_thresholds.treasury_withdrawal * 100.0) as i32, + denominator: 100, + }), + }), + pool_voting_thresholds: Some(u5c::PoolVotingThresholds { + motion_no_confidence: Some(u5c::RationalNumber { + numerator: (conway.pool_voting_thresholds.motion_no_confidence * 100.0) as i32, + denominator: 100, + }), + committee_normal: Some(u5c::RationalNumber { + numerator: (conway.pool_voting_thresholds.committee_normal * 100.0) as i32, + denominator: 100, + }), + committee_no_confidence: Some(u5c::RationalNumber { + numerator: (conway.pool_voting_thresholds.committee_no_confidence * 100.0) + as i32, + denominator: 100, + }), + hard_fork_initiation: Some(u5c::RationalNumber { + numerator: (conway.pool_voting_thresholds.hard_fork_initiation * 100.0) as i32, + denominator: 100, + }), + pp_security_group: Some(u5c::RationalNumber { + numerator: (conway.pool_voting_thresholds.pp_security_group * 100.0) as i32, + denominator: 100, + }), + }), + + // Byron complex structures + heavy_delegation: byron + .heavy_delegation + .iter() + .map(|(k, v)| { + ( + k.clone(), + u5c::HeavyDelegation { + omega: v.omega, + issuer_pk: v.issuer_pk.clone(), + delegate_pk: v.delegate_pk.clone(), + cert: v.cert.clone(), + }, + ) + }) + .collect(), + + // More Byron and other fields + block_version_data: Some(u5c::BlockVersionData { + script_version: byron.block_version_data.script_version as u32, + max_block_size: byron.block_version_data.max_block_size.to_string(), + max_tx_size: byron.block_version_data.max_tx_size.to_string(), + max_header_size: byron.block_version_data.max_header_size.to_string(), + max_proposal_size: byron.block_version_data.max_proposal_size.to_string(), + mpc_thd: byron.block_version_data.mpc_thd.to_string(), + heavy_del_thd: byron.block_version_data.heavy_del_thd.to_string(), + slot_duration: byron.block_version_data.slot_duration.to_string(), + update_vote_thd: byron.block_version_data.update_vote_thd.to_string(), + update_proposal_thd: byron.block_version_data.update_proposal_thd.to_string(), + update_implicit: byron.block_version_data.update_implicit.to_string(), + softfork_rule: Some(u5c::SoftforkRule { + init_thd: byron.block_version_data.softfork_rule.init_thd.to_string(), + min_thd: byron.block_version_data.softfork_rule.min_thd.to_string(), + thd_decrement: byron + .block_version_data + .softfork_rule + .thd_decrement + .to_string(), + }), + tx_fee_policy: Some(u5c::TxFeePolicy { + summand: byron.block_version_data.tx_fee_policy.summand.to_string(), + multiplier: byron + .block_version_data + .tx_fee_policy + .multiplier + .to_string(), + }), + unlock_stake_epoch: byron.block_version_data.unlock_stake_epoch.to_string(), + }), + fts_seed: byron.fts_seed.clone().unwrap_or_default(), + protocol_consts: Some(u5c::ProtocolConsts { + k: byron.protocol_consts.k as u32, + protocol_magic: byron.protocol_consts.protocol_magic as u32, + vss_min_ttl: byron.protocol_consts.vss_min_ttl.unwrap_or(0), + vss_max_ttl: byron.protocol_consts.vss_max_ttl.unwrap_or(0), + }), + vss_certs: byron + .vss_certs + .as_ref() + .map(|certs| { + certs + .iter() + .map(|(k, v)| { + ( + k.clone(), + u5c::VssCert { + vss_key: v.vss_key.clone(), + expiry_epoch: v.expiry_epoch, + signature: v.signature.clone(), + signing_key: v.signing_key.clone(), + }, + ) + }) + .collect() + }) + .unwrap_or_default(), + protocol_params: current_params.map(|params| self.map_pparams(params)), + cost_models: Some(u5c::CostModelMap { + plutus_v1: alonzo + .cost_models + .get(&alonzo::Language::PlutusV1) + .map(|v| u5c::CostModel { + values: v.clone().into(), + }), + plutus_v2: alonzo + .cost_models + .get(&alonzo::Language::PlutusV2) + .map(|v| u5c::CostModel { + values: v.clone().into(), + }), + plutus_v3: Some(u5c::CostModel { + values: conway.plutus_v3_cost_model.clone(), + }), + }), + } + } + + /// Map era summaries using HFC data from GenesisValues + /// Includes all eras, even when slot data is missing (will use None for unknown slots) + pub fn map_era_summaries( + &self, + current_params: Option, + ) -> u5c::EraSummaries { + // Include all eras that exist, not just ones with available slot data + let all_eras = self.genesis.available_eras(); + + let summaries = all_eras + .iter() + .enumerate() + .map(|(i, era)| { + let start_slot = self.genesis.era_start_slot(*era); + let end_slot = if i < all_eras.len() - 1 { + self.genesis.era_start_slot(all_eras[i + 1]) + } else { + None // Current era has no end + }; + + let is_current_era = i == all_eras.len() - 1; + + u5c::EraSummary { + name: era.to_string().to_lowercase(), + start: start_slot.map(|slot| { + let (epoch, _) = self.genesis.absolute_slot_to_relative(slot); + u5c::EraBoundary { + time: self.genesis.slot_to_wallclock(slot), + slot, + epoch, + } + }), + end: end_slot.map(|slot| { + let (epoch, _) = self.genesis.absolute_slot_to_relative(slot); + u5c::EraBoundary { + time: self.genesis.slot_to_wallclock(slot), + slot, + epoch, + } + }), + protocol_params: if is_current_era { + current_params + .as_ref() + .map(|params| self.map_pparams(params.clone())) + } else { + None + }, + } + }) + .collect(); + + u5c::EraSummaries { summaries } + } +} diff --git a/pallas-utxorpc/src/lib.rs b/pallas-utxorpc/src/lib.rs index 7f1826fd1..182b4f568 100644 --- a/pallas-utxorpc/src/lib.rs +++ b/pallas-utxorpc/src/lib.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, ops::Deref}; use pallas_codec::utils::KeyValuePairs; use pallas_crypto::hash::Hash; use pallas_primitives::{alonzo, babbage, conway}; -use pallas_traverse::{self as trv}; +use pallas_traverse::{self as trv, wellknown::GenesisValues}; use prost_types::FieldMask; use trv::OriginalHash; @@ -13,6 +13,7 @@ pub use utxorpc_spec::utxorpc::v1alpha as spec; use utxorpc_spec::utxorpc::v1alpha::cardano as u5c; mod certs; +mod genesis; mod params; pub type TxHash = Hash<32>; @@ -35,16 +36,36 @@ pub trait LedgerContext: Clone { fn get_slot_timestamp(&self, slot: u64) -> Option; } -#[derive(Default, Clone)] +#[derive(Clone)] pub struct Mapper { ledger: Option, + genesis: GenesisValues, _mask: FieldMask, } +impl Default for Mapper { + fn default() -> Self { + Self { + ledger: None, + genesis: GenesisValues::default(), + _mask: FieldMask { paths: vec![] }, + } + } +} + impl Mapper { pub fn new(ledger: C) -> Self { Self { ledger: Some(ledger), + genesis: GenesisValues::default(), + _mask: FieldMask { paths: vec![] }, + } + } + + pub fn new_with_genesis(ledger: C, genesis: GenesisValues) -> Self { + Self { + ledger: Some(ledger), + genesis, _mask: FieldMask { paths: vec![] }, } } @@ -53,6 +74,7 @@ impl Mapper { pub fn masked(&self, mask: FieldMask) -> Self { Self { ledger: self.ledger.clone(), + genesis: self.genesis.clone(), _mask: mask, } } @@ -722,11 +744,7 @@ impl Mapper { tx: block.txs().iter().map(|x| self.map_tx(x)).collect(), } .into(), - timestamp: self - .ledger - .as_ref() - .and_then(|ledger| ledger.get_slot_timestamp(block.slot())) - .unwrap_or(0), + timestamp: block.wallclock(&self.genesis), } }