diff --git a/Cargo.lock b/Cargo.lock index 664e62fb..38eda229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ dependencies = [ "config", "hex", "pallas", - "reqwest 0.11.27", + "reqwest 0.12.23", "serde_json", "tokio", "tracing", diff --git a/common/src/calculations.rs b/common/src/calculations.rs index 4d01eb9d..1f03251e 100644 --- a/common/src/calculations.rs +++ b/common/src/calculations.rs @@ -1,25 +1,32 @@ //! Common calculations for Cardano const BYRON_SLOTS_PER_EPOCH: u64 = 21_600; -pub const SHELLEY_SLOTS_PER_EPOCH: u64 = 432_000; -const SHELLEY_START_SLOT: u64 = 4_492_800; -const SHELLEY_START_EPOCH: u64 = 208; /// Derive an epoch number from a slot, handling Byron/Shelley era changes -pub fn slot_to_epoch(slot: u64) -> u64 { - slot_to_epoch_with_shelley_params(slot, SHELLEY_START_EPOCH, SHELLEY_SLOTS_PER_EPOCH) -} - pub fn slot_to_epoch_with_shelley_params( slot: u64, shelley_epoch: u64, shelley_epoch_len: u64, -) -> u64 { +) -> (u64, u64) { let shelley_start_slot = shelley_epoch * BYRON_SLOTS_PER_EPOCH; if slot < shelley_start_slot { - slot / BYRON_SLOTS_PER_EPOCH + (slot / BYRON_SLOTS_PER_EPOCH, slot % BYRON_SLOTS_PER_EPOCH) } else { - shelley_epoch + (slot - shelley_start_slot) / shelley_epoch_len + let slots_since_shelley_start = slot - shelley_start_slot; + ( + shelley_epoch + slots_since_shelley_start / shelley_epoch_len, + slots_since_shelley_start % shelley_epoch_len, + ) + } +} + +pub fn slot_to_timestamp_with_params(slot: u64, byron_timestamp: u64, shelley_epoch: u64) -> u64 { + let shelley_start_slot = shelley_epoch * BYRON_SLOTS_PER_EPOCH; + if slot < shelley_start_slot { + byron_timestamp + slot * 20 + } else { + let shelley_timestamp = byron_timestamp + shelley_start_slot * 20; + shelley_timestamp + (slot - shelley_start_slot) } } @@ -27,48 +34,53 @@ pub fn slot_to_epoch_with_shelley_params( #[cfg(test)] mod tests { use super::*; + const SHELLEY_START_EPOCH: u64 = 208; + const SHELLEY_SLOTS_PER_EPOCH: u64 = 432_000; + const BYRON_START_TIMESTAMP: u64 = 1506203091; + + fn slot_to_epoch(slot: u64) -> (u64, u64) { + slot_to_epoch_with_shelley_params(slot, SHELLEY_START_EPOCH, SHELLEY_SLOTS_PER_EPOCH) + } + + fn slot_to_timestamp(slot: u64) -> u64 { + slot_to_timestamp_with_params(slot, BYRON_START_TIMESTAMP, SHELLEY_START_EPOCH) + } #[test] fn byron_epoch_0() { - assert_eq!(0, slot_to_epoch(0)); + assert_eq!(slot_to_epoch(0), (0, 0)); + assert_eq!(slot_to_timestamp(0), 1506203091); } #[test] fn byron_epoch_1() { - assert_eq!(1, slot_to_epoch(21_600)); + assert_eq!(slot_to_epoch(21_600), (1, 0)); + assert_eq!(slot_to_timestamp(21_600), 1506635091); } #[test] fn byron_last_slot() { - assert_eq!(slot_to_epoch(4_492_799), 207); + assert_eq!(slot_to_epoch(4_492_799), (207, 21_599)); + assert_eq!(slot_to_timestamp(4_492_799), 1596059071); } #[test] fn shelley_first_slot() { - assert_eq!(slot_to_epoch(4_492_800), 208); + assert_eq!(slot_to_epoch(4_492_800), (208, 0)); + assert_eq!(slot_to_timestamp(4_492_800), 1596059091); } #[test] fn shelley_epoch_209_start() { // 432_000 slots later - assert_eq!(slot_to_epoch(4_492_800 + 432_000), 209); - } - - #[test] - fn before_transition_boundary() { - // One slot before Shelley starts - assert_eq!(slot_to_epoch(4_492_799), 207); - } - - #[test] - fn after_transition_boundary() { - // First Shelley slot - assert_eq!(slot_to_epoch(4_492_800), 208); + assert_eq!(slot_to_epoch(4_492_800 + 432_000), (209, 0)); + assert_eq!(slot_to_timestamp(4_492_800 + 432_000), 1596491091); } #[test] fn mainnet_example_from_cexplorer() { // Slot 98_272_003 maps to epoch 425 - assert_eq!(slot_to_epoch(98_272_003), 425); + assert_eq!(slot_to_epoch(98_272_003), (425, 35_203)); + assert_eq!(slot_to_timestamp(98_272_003), 1689838294); } } diff --git a/common/src/genesis_values.rs b/common/src/genesis_values.rs new file mode 100644 index 00000000..0c0503f4 --- /dev/null +++ b/common/src/genesis_values.rs @@ -0,0 +1,25 @@ +use crate::calculations::{slot_to_epoch_with_shelley_params, slot_to_timestamp_with_params}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenesisValues { + pub byron_timestamp: u64, + pub shelley_epoch: u64, + pub shelley_epoch_len: u64, +} + +impl GenesisValues { + pub fn mainnet() -> Self { + Self { + byron_timestamp: 1506203091, + shelley_epoch: 208, + shelley_epoch_len: 432000, + } + } + + pub fn slot_to_epoch(&self, slot: u64) -> (u64, u64) { + slot_to_epoch_with_shelley_params(slot, self.shelley_epoch, self.shelley_epoch_len) + } + pub fn slot_to_timestamp(&self, slot: u64) -> u64 { + slot_to_timestamp_with_params(slot, self.byron_timestamp, self.shelley_epoch) + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index a44d67d7..2f32cbdf 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -4,6 +4,7 @@ pub mod address; pub mod calculations; pub mod cip19; pub mod crypto; +pub mod genesis_values; pub mod ledger_state; pub mod messages; pub mod params; diff --git a/common/src/messages.rs b/common/src/messages.rs index 9acb250b..dcea2e3c 100644 --- a/common/src/messages.rs +++ b/common/src/messages.rs @@ -3,6 +3,7 @@ // We don't use these messages in the acropolis_common crate itself #![allow(dead_code)] +use crate::genesis_values::GenesisValues; use crate::ledger_state::SPOState; use crate::protocol_params::ProtocolParams; use crate::queries::parameters::{ParametersStateQuery, ParametersStateQueryResponse}; @@ -58,7 +59,9 @@ pub struct RawTxsMessage { /// Genesis completion message #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct GenesisCompleteMessage {} +pub struct GenesisCompleteMessage { + pub values: GenesisValues, +} /// Message encapsulating multiple UTXO deltas, in order #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/types.rs b/common/src/types.rs index 19c10698..1272b2b6 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -100,9 +100,15 @@ pub struct BlockInfo { /// Epoch number pub epoch: u64, + /// Epoch slot number + pub epoch_slot: u64, + /// Does this block start a new epoch? pub new_epoch: bool, + /// UNIX timestamp + pub timestamp: u64, + /// Protocol era pub era: Era, } diff --git a/modules/epoch_activity_counter/src/state.rs b/modules/epoch_activity_counter/src/state.rs index 6fd5cb3b..e04a2d89 100644 --- a/modules/epoch_activity_counter/src/state.rs +++ b/modules/epoch_activity_counter/src/state.rs @@ -133,7 +133,9 @@ mod tests { number: 42, hash: Vec::new(), epoch, + epoch_slot: 99, new_epoch: false, + timestamp: 99999, era: Era::Conway, } } diff --git a/modules/genesis_bootstrapper/Cargo.toml b/modules/genesis_bootstrapper/Cargo.toml index 452b8bc5..435ed125 100644 --- a/modules/genesis_bootstrapper/Cargo.toml +++ b/modules/genesis_bootstrapper/Cargo.toml @@ -21,7 +21,9 @@ tokio = { version = "1", features = ["full"] } tracing = "0.1.40" [build-dependencies] -reqwest = { version = "0.11", features = ["blocking"] } +anyhow = "1.0" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +tokio = { version = "1", features = ["full"] } [lib] path = "src/genesis_bootstrapper.rs" diff --git a/modules/genesis_bootstrapper/build.rs b/modules/genesis_bootstrapper/build.rs index 9921dbfb..81caaccc 100644 --- a/modules/genesis_bootstrapper/build.rs +++ b/modules/genesis_bootstrapper/build.rs @@ -1,36 +1,60 @@ // Build-time script to download generics -use reqwest::blocking::get; use std::fs; use std::io::Write; use std::path::Path; +use anyhow::{Context, Result}; + const OUTPUT_DIR: &str = "downloads"; /// Download a URL to a file in OUTPUT_DIR -fn download(url: &str, filename: &str) { - let response = get(url).expect("Failed to fetch {url}"); - let data = response.text().expect("Failed to read response"); +async fn download(client: &reqwest::Client, url: &str, filename: &str) -> Result<()> { + let request = client.get(url).build().with_context(|| format!("Failed to request {url}"))?; + let response = + client.execute(request).await.with_context(|| format!("Failed to fetch {url}"))?; + let data = response.text().await.context("Failed to read response")?; let output_path = Path::new(OUTPUT_DIR); if !output_path.exists() { - fs::create_dir_all(output_path).expect("Failed to create {OUTPUT_DIR} directory"); + fs::create_dir_all(output_path) + .with_context(|| format!("Failed to create {OUTPUT_DIR} directory"))?; } let file_path = output_path.join(filename); - let mut file = fs::File::create(&file_path).expect("Failed to create file {file_path}"); - file.write_all(data.as_bytes()).expect("Failed to write file {file_path}"); + let mut file = fs::File::create(&file_path) + .with_context(|| format!("Failed to create file {}", file_path.display()))?; + file.write_all(data.as_bytes()) + .with_context(|| format!("Failed to write file {}", file_path.display()))?; + Ok(()) } -fn main() { +#[tokio::main] +async fn main() -> Result<()> { println!("cargo:rerun-if-changed=build.rs"); // Ensure the script runs if modified + let client = reqwest::Client::new(); - download( - "https://book.world.dev.cardano.org/environments/mainnet/byron-genesis.json", - "mainnet-byron-genesis.json", - ); + tokio::try_join!( + download( + &client, + "https://book.world.dev.cardano.org/environments/mainnet/byron-genesis.json", + "mainnet-byron-genesis.json", + ), + download( + &client, + "https://book.world.dev.cardano.org/environments/mainnet/shelley-genesis.json", + "mainnet-shelley-genesis.json", + ), + download( + &client, + "https://raw.githubusercontent.com/Hornan7/SanchoNet-Tutorials/refs/heads/main/genesis/byron-genesis.json", + "sanchonet-byron-genesis.json", + ), + download( + &client, + "https://raw.githubusercontent.com/Hornan7/SanchoNet-Tutorials/refs/heads/main/genesis/shelley-genesis.json", + "sanchonet-shelley-genesis.json", + ) + )?; - download( - "https://raw.githubusercontent.com/Hornan7/SanchoNet-Tutorials/refs/heads/main/genesis/byron-genesis.json", - "sanchonet-byron-genesis.json", - ); + Ok(()) } diff --git a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs index d53d152e..96443d09 100644 --- a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs +++ b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs @@ -2,6 +2,7 @@ //! Reads genesis files and outputs initial UTXO events use acropolis_common::{ + genesis_values::GenesisValues, messages::{ CardanoMessage, GenesisCompleteMessage, Message, PotDeltasMessage, UTXODeltasMessage, }, @@ -23,7 +24,12 @@ const DEFAULT_NETWORK_NAME: &str = "mainnet"; // Include genesis data (downloaded by build.rs) const MAINNET_BYRON_GENESIS: &[u8] = include_bytes!("../downloads/mainnet-byron-genesis.json"); +const MAINNET_SHELLEY_GENESIS: &[u8] = include_bytes!("../downloads/mainnet-shelley-genesis.json"); +const MAINNET_SHELLEY_START_EPOCH: u64 = 208; const SANCHONET_BYRON_GENESIS: &[u8] = include_bytes!("../downloads/sanchonet-byron-genesis.json"); +const SANCHONET_SHELLEY_GENESIS: &[u8] = + include_bytes!("../downloads/sanchonet-shelley-genesis.json"); +const SANCHONET_SHELLEY_START_EPOCH: u64 = 0; // Initial reserves (=maximum ever Lovelace supply) const INITIAL_RESERVES: Lovelace = 45_000_000_000_000_000; @@ -70,19 +76,30 @@ impl GenesisBootstrapper { let network_name = config.get_string("network-name").unwrap_or(DEFAULT_NETWORK_NAME.to_string()); - let genesis = match network_name.as_ref() { - "mainnet" => MAINNET_BYRON_GENESIS, - "sanchonet" => SANCHONET_BYRON_GENESIS, - _ => { - error!("Cannot find genesis for {network_name}"); - return; - } - }; + let (byron_genesis, shelley_genesis, shelley_start_epoch) = + match network_name.as_ref() { + "mainnet" => ( + MAINNET_BYRON_GENESIS, + MAINNET_SHELLEY_GENESIS, + MAINNET_SHELLEY_START_EPOCH, + ), + "sanchonet" => ( + SANCHONET_BYRON_GENESIS, + SANCHONET_SHELLEY_GENESIS, + SANCHONET_SHELLEY_START_EPOCH, + ), + _ => { + error!("Cannot find genesis for {network_name}"); + return; + } + }; info!("Reading genesis for '{network_name}'"); // Read genesis data - let genesis: byron::GenesisFile = - serde_json::from_slice(genesis).expect("Invalid JSON in BYRON_GENESIS file"); + let byron_genesis: byron::GenesisFile = serde_json::from_slice(byron_genesis) + .expect("Invalid JSON in BYRON_GENESIS file"); + let shelley_genesis: shelley::GenesisFile = serde_json::from_slice(shelley_genesis) + .expect("Invalid JSON in SHELLEY_GENESIS file"); // Construct messages let block_info = BlockInfo { @@ -91,14 +108,16 @@ impl GenesisBootstrapper { number: 0, hash: Vec::new(), epoch: 0, + epoch_slot: 0, new_epoch: false, + timestamp: byron_genesis.start_time, era: Era::Byron, }; let mut utxo_deltas_message = UTXODeltasMessage { deltas: Vec::new() }; // Convert the AVVM distributions into pseudo-UTXOs - let gen_utxos = genesis_utxos(&genesis); + let gen_utxos = genesis_utxos(&byron_genesis); let mut total_allocated: u64 = 0; for (hash, address, amount) in gen_utxos.iter() { let tx_output = TxOutput { @@ -145,10 +164,16 @@ impl GenesisBootstrapper { .await .unwrap_or_else(|e| error!("Failed to publish: {e}")); + let values = GenesisValues { + byron_timestamp: byron_genesis.start_time, + shelley_epoch: shelley_start_epoch, + shelley_epoch_len: shelley_genesis.epoch_length.unwrap() as u64, + }; + // Send completion message let message_enum = Message::Cardano(( block_info, - CardanoMessage::GenesisComplete(GenesisCompleteMessage {}), + CardanoMessage::GenesisComplete(GenesisCompleteMessage { values }), )); context .message_bus diff --git a/modules/governance_state/src/alonzo_babbage_voting.rs b/modules/governance_state/src/alonzo_babbage_voting.rs index 9aaa387e..f57dcd96 100644 --- a/modules/governance_state/src/alonzo_babbage_voting.rs +++ b/modules/governance_state/src/alonzo_babbage_voting.rs @@ -1,30 +1,38 @@ use acropolis_common::{ - calculations::SHELLEY_SLOTS_PER_EPOCH, AlonzoBabbageUpdateProposal, AlonzoBabbageVotingOutcome, - BlockInfo, Era, GenesisKeyhash, ProtocolParamUpdate, + AlonzoBabbageUpdateProposal, AlonzoBabbageVotingOutcome, BlockInfo, Era, GenesisKeyhash, + ProtocolParamUpdate, }; use anyhow::{bail, Result}; use std::collections::{HashMap, HashSet}; const GENESIS_KEYS_VOTES_THRESHOLD: u64 = 5; +const MAINNET_SHELLEY_SLOTS_PER_EPOCH: u64 = 432_000; -#[derive(Default)] pub struct AlonzoBabbageVoting { /// map "enact epoch" (proposal enacts at this epoch end) to voting /// "voting": map voter (genesis key) => (vote epoch, vote slot, proposal) /// "vote epoch/slot" --- moment, when the vote was cast for the proposal proposals: HashMap)>>, + shelley_slots_per_epoch: u64, } impl AlonzoBabbageVoting { pub fn new() -> Self { - Self::default() + Self { + proposals: HashMap::new(), + shelley_slots_per_epoch: MAINNET_SHELLEY_SLOTS_PER_EPOCH, + } } /// Vote is counted for the new epoch if cast in previous epoch /// before 4/10 of its start (not too fresh). /// Here is it: [!++++++++++!++++------!] - fn is_timely_vote(_epoch: u64, slot: u64, new_block: &BlockInfo) -> bool { - slot + (6 * SHELLEY_SLOTS_PER_EPOCH / 10) < new_block.slot + fn is_timely_vote(&self, _epoch: u64, slot: u64, new_block: &BlockInfo) -> bool { + slot + (6 * self.shelley_slots_per_epoch / 10) < new_block.slot + } + + pub fn update_shelley_slots_per_epoch(&mut self, shelley_slots_per_epoch: u64) { + self.shelley_slots_per_epoch = shelley_slots_per_epoch; } pub fn process_update_proposals( @@ -62,7 +70,7 @@ impl AlonzoBabbageVoting { let proposals = proposals_for_new_epoch .iter() - .filter(|(_k, (epoch, slot, _proposal))| Self::is_timely_vote(*epoch, *slot, new_blk)) + .filter(|(_k, (epoch, slot, _proposal))| self.is_timely_vote(*epoch, *slot, new_blk)) .map(|(k, (_e, _s, proposal))| (k.clone(), proposal.clone())) .collect::>(); @@ -104,7 +112,7 @@ impl AlonzoBabbageVoting { #[cfg(test)] mod tests { - use crate::alonzo_babbage_voting::AlonzoBabbageVoting; + use crate::alonzo_babbage_voting::{AlonzoBabbageVoting, MAINNET_SHELLEY_SLOTS_PER_EPOCH}; use acropolis_common::{ rational_number::rational_number_from_f32, AlonzoBabbageUpdateProposal, AlonzoBabbageVotingOutcome, BlockInfo, BlockStatus, GenesisKeyhash, ProtocolParamUpdate, @@ -138,8 +146,10 @@ mod tests { slot, number: slot, epoch, + epoch_slot: epoch % MAINNET_SHELLEY_SLOTS_PER_EPOCH, era: era.try_into()?, new_epoch: new_epoch != 0, + timestamp: 0, hash: Vec::new(), }; diff --git a/modules/governance_state/src/state.rs b/modules/governance_state/src/state.rs index 0d231212..6c358aad 100644 --- a/modules/governance_state/src/state.rs +++ b/modules/governance_state/src/state.rs @@ -77,6 +77,9 @@ impl State { &mut self, message: &ProtocolParamsMessage, ) -> Result<()> { + if let Some(shelley) = &message.params.shelley { + self.alonzo_babbage_voting.update_shelley_slots_per_epoch(shelley.epoch_length as u64); + } if message.params.conway.is_some() { self.conway = message.params.conway.clone(); } diff --git a/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs b/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs index 447ac84f..64f07e31 100644 --- a/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs +++ b/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs @@ -2,7 +2,7 @@ //! Fetches a snapshot from Mithril and replays all the blocks in it use acropolis_common::{ - calculations::slot_to_epoch, + genesis_values::GenesisValues, messages::{BlockBodyMessage, BlockHeaderMessage, CardanoMessage, Message}, BlockInfo, BlockStatus, Era, }; @@ -233,7 +233,11 @@ impl MithrilSnapshotFetcher { } /// Process the snapshot - async fn process_snapshot(context: Arc>, config: Arc) -> Result<()> { + async fn process_snapshot( + context: Arc>, + config: Arc, + genesis: GenesisValues, + ) -> Result<()> { let header_topic = config.get_string("header-topic").unwrap_or(DEFAULT_HEADER_TOPIC.to_string()); let body_topic = config.get_string("body-topic").unwrap_or(DEFAULT_BODY_TOPIC.to_string()); @@ -290,7 +294,7 @@ impl MithrilSnapshotFetcher { } last_block_number = number; - let epoch = slot_to_epoch(slot); + let (epoch, epoch_slot) = genesis.slot_to_epoch(slot); let new_epoch = match last_epoch { Some(last_epoch) => epoch != last_epoch, None => true, @@ -301,6 +305,8 @@ impl MithrilSnapshotFetcher { info!(epoch, number, slot, "New epoch"); } + let timestamp = genesis.slot_to_timestamp(slot); + let era = match block.era() { PallasEra::Byron => Era::Byron, PallasEra::Shelley => Era::Shelley, @@ -317,7 +323,9 @@ impl MithrilSnapshotFetcher { number, hash: block.hash().to_vec(), epoch, + epoch_slot, new_epoch, + timestamp, era, }; @@ -397,10 +405,16 @@ impl MithrilSnapshotFetcher { let mut subscription = context.subscribe(&startup_topic).await?; context.clone().run(async move { - let Ok(_) = subscription.read().await else { + let Ok((_, startup_message)) = subscription.read().await else { return; }; info!("Received startup message"); + let genesis = match startup_message.as_ref() { + Message::Cardano((_, CardanoMessage::GenesisComplete(complete))) => { + complete.values.clone() + } + x => panic!("unexpected startup message: {x:?}"), + }; let mut delay = 1; loop { @@ -416,7 +430,7 @@ impl MithrilSnapshotFetcher { delay = (delay * 2).min(60); } - match Self::process_snapshot(context, config).await { + match Self::process_snapshot(context, config, genesis).await { Err(e) => error!("Failed to process Mithril snapshot: {e}"), _ => {} } diff --git a/modules/spo_state/src/test_utils.rs b/modules/spo_state/src/test_utils.rs index 6ea99f84..055c4e27 100644 --- a/modules/spo_state/src/test_utils.rs +++ b/modules/spo_state/src/test_utils.rs @@ -50,7 +50,9 @@ pub fn new_block(epoch: u64) -> BlockInfo { number: 10 * epoch, hash: Vec::::new(), epoch, + epoch_slot: 0, new_epoch: true, + timestamp: epoch, era: Era::Byron, } } diff --git a/modules/stake_delta_filter/src/utils.rs b/modules/stake_delta_filter/src/utils.rs index d8d66a19..6f7a01c9 100644 --- a/modules/stake_delta_filter/src/utils.rs +++ b/modules/stake_delta_filter/src/utils.rs @@ -541,7 +541,9 @@ mod test { number: 1, hash: vec![], epoch: 1, + epoch_slot: 14243, new_epoch: true, + timestamp: 2498243, era: Era::Conway, }; diff --git a/modules/upstream_chain_fetcher/src/body_fetcher.rs b/modules/upstream_chain_fetcher/src/body_fetcher.rs index 6537d53d..54a19eae 100644 --- a/modules/upstream_chain_fetcher/src/body_fetcher.rs +++ b/modules/upstream_chain_fetcher/src/body_fetcher.rs @@ -83,12 +83,13 @@ impl BodyFetcher { let number = header.number(); let hash = header.hash().to_vec(); - let epoch = self.cfg.slot_to_epoch(slot); + let (epoch, epoch_slot) = self.cfg.slot_to_epoch(slot); let new_epoch = match self.last_epoch { Some(last_epoch) => epoch != last_epoch, None => true, }; self.last_epoch = Some(epoch); + let timestamp = self.cfg.slot_to_timestamp(slot); if new_epoch { info!(epoch, number, slot, "New epoch"); @@ -116,7 +117,9 @@ impl BodyFetcher { number, hash: hash.clone(), epoch, + epoch_slot, new_epoch, + timestamp, era, }; diff --git a/modules/upstream_chain_fetcher/src/upstream_cache.rs b/modules/upstream_chain_fetcher/src/upstream_cache.rs index 3cd70ca8..5a3332c4 100644 --- a/modules/upstream_chain_fetcher/src/upstream_cache.rs +++ b/modules/upstream_chain_fetcher/src/upstream_cache.rs @@ -184,7 +184,9 @@ mod test { number: n, hash: vec![], epoch: 0, + epoch_slot: n, new_epoch: false, + timestamp: n, era: Era::default(), } } diff --git a/modules/upstream_chain_fetcher/src/upstream_chain_fetcher.rs b/modules/upstream_chain_fetcher/src/upstream_chain_fetcher.rs index e21afee1..a6203ba5 100644 --- a/modules/upstream_chain_fetcher/src/upstream_chain_fetcher.rs +++ b/modules/upstream_chain_fetcher/src/upstream_chain_fetcher.rs @@ -2,6 +2,7 @@ //! Multi-connection, multi-protocol client interface to the Cardano node use acropolis_common::{ + genesis_values::GenesisValues, messages::{CardanoMessage, Message}, BlockInfo, }; @@ -62,8 +63,8 @@ impl UpstreamChainFetcher { let mut response_count = 0; let last_epoch: Option = match slot { - 0 => None, // If we're starting from origin - _ => Some(cfg.slot_to_epoch(slot)), // From slot of last block + 0 => None, // If we're starting from origin + _ => Some(cfg.slot_to_epoch(slot).0), // From slot of last block }; let (sender, receiver) = bounded(MAX_BODY_FETCHER_CHANNEL_LENGTH); @@ -141,6 +142,18 @@ impl UpstreamChainFetcher { Ok(last_block) } + async fn wait_genesis_completion( + subscription: &mut Box>, + ) -> Result { + let (_, message) = subscription.read().await?; + match message.as_ref() { + Message::Cardano((_, CardanoMessage::GenesisComplete(complete))) => { + Ok(complete.values.clone()) + } + msg => bail!("Unexpected message in genesis completion topic: {msg:?}"), + } + } + async fn wait_snapshot_completion( subscription: &mut Box>, ) -> Result> { @@ -209,8 +222,13 @@ impl UpstreamChainFetcher { /// Main init function pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { - let cfg = FetcherConfig::new(context.clone(), config)?; - let mut subscription = match cfg.sync_point { + let mut cfg = FetcherConfig::new(context.clone(), config)?; + let genesis_complete = if cfg.genesis_values.is_none() { + Some(cfg.context.subscribe(&cfg.genesis_completion_topic).await?) + } else { + None + }; + let mut snapshot_complete = match cfg.sync_point { SyncPoint::Snapshot => { Some(cfg.context.subscribe(&cfg.snapshot_completion_topic).await?) } @@ -218,7 +236,14 @@ impl UpstreamChainFetcher { }; context.clone().run(async move { - Self::run_chain_sync(cfg, &mut subscription) + if let Some(mut genesis_complete) = genesis_complete { + let genesis = Self::wait_genesis_completion(&mut genesis_complete) + .await + .unwrap_or_else(|err| panic!("could not fetch genesis: {err}")); + cfg.genesis_values = Some(genesis); + } + let cfg = Arc::new(cfg); + Self::run_chain_sync(cfg, &mut snapshot_complete) .await .unwrap_or_else(|e| error!("Chain sync failed: {e}")); }); diff --git a/modules/upstream_chain_fetcher/src/utils.rs b/modules/upstream_chain_fetcher/src/utils.rs index 57fa6b77..525430cd 100644 --- a/modules/upstream_chain_fetcher/src/utils.rs +++ b/modules/upstream_chain_fetcher/src/utils.rs @@ -1,5 +1,5 @@ use crate::UpstreamCacheRecord; -use acropolis_common::calculations::slot_to_epoch_with_shelley_params; +use acropolis_common::genesis_values::GenesisValues; use acropolis_common::messages::{CardanoMessage, Message}; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::Context; @@ -12,7 +12,9 @@ use tracing::info; const DEFAULT_HEADER_TOPIC: (&str, &str) = ("header-topic", "cardano.block.header"); const DEFAULT_BODY_TOPIC: (&str, &str) = ("body-topic", "cardano.block.body"); const DEFAULT_SNAPSHOT_COMPLETION_TOPIC: (&str, &str) = - ("snapshot-complietion-topic", "cardano.snapshot.complete"); + ("snapshot-completion-topic", "cardano.snapshot.complete"); +const DEFAULT_GENESIS_COMPLETION_TOPIC: (&str, &str) = + ("genesis-completion-topic", "cardano.sequence.bootstrapped"); const DEFAULT_NODE_ADDRESS: (&str, &str) = ("node-address", "backbone.cardano.iog.io:3001"); const DEFAULT_MAGIC_NUMBER: (&str, u64) = ("magic-number", 764824073); @@ -20,8 +22,9 @@ const DEFAULT_MAGIC_NUMBER: (&str, u64) = ("magic-number", 764824073); const DEFAULT_SYNC_POINT: (&str, SyncPoint) = ("sync-point", SyncPoint::Snapshot); const DEFAULT_CACHE_DIR: (&str, &str) = ("cache-dir", "upstream-cache"); -const DEFAULT_SHELLEY_EPOCH: (&str, u64) = ("shelley-epoch", 208); -const DEFAULT_SHELLEY_EPOCH_LEN: (&str, u64) = ("shelley-epoch-len", 432000); +const BYRON_TIMESTAMP: &str = "byron-timestamp"; +const SHELLEY_EPOCH: &str = "shelley-epoch"; +const SHELLEY_EPOCH_LEN: &str = "shelley-epoch-len"; #[derive(Clone, Debug, serde::Deserialize, PartialEq)] pub enum SyncPoint { @@ -41,12 +44,12 @@ pub struct FetcherConfig { pub body_topic: String, pub sync_point: SyncPoint, pub snapshot_completion_topic: String, + pub genesis_completion_topic: String, pub node_address: String, pub magic_number: u64, pub cache_dir: String, - pub shelley_epoch: u64, - pub shelley_epoch_len: u64, + pub genesis_values: Option, } impl FetcherConfig { @@ -71,29 +74,40 @@ impl FetcherConfig { Ok(actual) } - pub fn new(context: Arc>, config: Arc) -> Result> { - Ok(Arc::new(Self { + fn conf_genesis(config: &Arc) -> Option { + let byron_timestamp = config.get(BYRON_TIMESTAMP).ok()?; + let shelley_epoch = config.get(SHELLEY_EPOCH).ok()?; + let shelley_epoch_len = config.get(SHELLEY_EPOCH_LEN).ok()?; + Some(GenesisValues { + byron_timestamp, + shelley_epoch, + shelley_epoch_len, + }) + } + + pub fn new(context: Arc>, config: Arc) -> Result { + Ok(Self { context, header_topic: Self::conf(&config, DEFAULT_HEADER_TOPIC), body_topic: Self::conf(&config, DEFAULT_BODY_TOPIC), snapshot_completion_topic: Self::conf(&config, DEFAULT_SNAPSHOT_COMPLETION_TOPIC), + genesis_completion_topic: Self::conf(&config, DEFAULT_GENESIS_COMPLETION_TOPIC), sync_point: Self::conf_enum::(&config, DEFAULT_SYNC_POINT)?, magic_number: config .get::(DEFAULT_MAGIC_NUMBER.0) .unwrap_or(DEFAULT_MAGIC_NUMBER.1), node_address: Self::conf(&config, DEFAULT_NODE_ADDRESS), cache_dir: Self::conf(&config, DEFAULT_CACHE_DIR), - shelley_epoch: config - .get::(DEFAULT_SHELLEY_EPOCH.0) - .unwrap_or(DEFAULT_SHELLEY_EPOCH.1), - shelley_epoch_len: config - .get::(DEFAULT_SHELLEY_EPOCH_LEN.0) - .unwrap_or(DEFAULT_SHELLEY_EPOCH_LEN.1), - })) + genesis_values: Self::conf_genesis(&config), + }) + } + + pub fn slot_to_epoch(&self, slot: u64) -> (u64, u64) { + self.genesis_values.as_ref().unwrap().slot_to_epoch(slot) } - pub fn slot_to_epoch(&self, slot: u64) -> u64 { - slot_to_epoch_with_shelley_params(slot, self.shelley_epoch, self.shelley_epoch_len) + pub fn slot_to_timestamp(&self, slot: u64) -> u64 { + self.genesis_values.as_ref().unwrap().slot_to_timestamp(slot) } } diff --git a/modules/utxo_state/src/state.rs b/modules/utxo_state/src/state.rs index 74e56a68..592624e8 100644 --- a/modules/utxo_state/src/state.rs +++ b/modules/utxo_state/src/state.rs @@ -414,7 +414,9 @@ mod tests { number, hash: vec![], epoch: 99, + epoch_slot: slot, new_epoch: false, + timestamp: slot, era: Era::Byron, } } diff --git a/processes/golden_tests/src/test_module.rs b/processes/golden_tests/src/test_module.rs index b0517611..2d606be7 100644 --- a/processes/golden_tests/src/test_module.rs +++ b/processes/golden_tests/src/test_module.rs @@ -48,7 +48,9 @@ impl TestModule { number: 1, hash: vec![], epoch: 1, + epoch_slot: 1, new_epoch: false, + timestamp: 1, era: Era::Conway, }, CardanoMessage::ReceivedTxs(RawTxsMessage { diff --git a/processes/omnibus/omnibus-sancho.toml b/processes/omnibus/omnibus-sancho.toml index 77e0ec92..8ba5e6f1 100644 --- a/processes/omnibus/omnibus-sancho.toml +++ b/processes/omnibus/omnibus-sancho.toml @@ -15,8 +15,6 @@ network-name = "sanchonet" # "sanchonet", "mainnet" sync-point = "cache" #"cache" # "origin", "tip", "snapshot" node-address = "sancho-testnet.able-pool.io:6002" magic-number = 4 -shelley-epoch = 0 -shelley-epoch-len = 86400 [module.block-unpacker]