Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion Cargo.lock

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

68 changes: 40 additions & 28 deletions common/src/calculations.rs
Original file line number Diff line number Diff line change
@@ -1,74 +1,86 @@
//! 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)
}
}

// -- Tests --
#[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);
}
}
25 changes: 25 additions & 0 deletions common/src/genesis_values.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion common/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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)]
Expand Down
6 changes: 6 additions & 0 deletions common/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
2 changes: 2 additions & 0 deletions modules/epoch_activity_counter/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ mod tests {
number: 42,
hash: Vec::new(),
epoch,
epoch_slot: 99,
new_epoch: false,
timestamp: 99999,
era: Era::Conway,
}
}
Expand Down
4 changes: 3 additions & 1 deletion modules/genesis_bootstrapper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
56 changes: 40 additions & 16 deletions modules/genesis_bootstrapper/build.rs
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shelley genesis has bugs, and cannot be properly parsed by Pallas, see parameters-state module.

)
)?;

download(
"https://raw.githubusercontent.com/Hornan7/SanchoNet-Tutorials/refs/heads/main/genesis/byron-genesis.json",
"sanchonet-byron-genesis.json",
);
Ok(())
}
49 changes: 37 additions & 12 deletions modules/genesis_bootstrapper/src/genesis_bootstrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! Reads genesis files and outputs initial UTXO events

use acropolis_common::{
genesis_values::GenesisValues,
messages::{
CardanoMessage, GenesisCompleteMessage, Message, PotDeltasMessage, UTXODeltasMessage,
},
Expand All @@ -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;
Copy link
Collaborator

@shd shd Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value is still hardcoded. Why not compute it?

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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd propose to use parameter-state module output here: it should provide all these data. If it does not, then it's much easier to add additional fields there.

};

// Send completion message
let message_enum = Message::Cardano((
block_info,
CardanoMessage::GenesisComplete(GenesisCompleteMessage {}),
CardanoMessage::GenesisComplete(GenesisCompleteMessage { values }),
));
context
.message_bus
Expand Down
Loading