use std::{
fs,
path::{Path, PathBuf},
};
use log::*;
use tari_transaction_components::consensus::ConsensusConstants;
const LOG_TARGET: &str = "c::cs::consensus_tracker";
pub struct ConsensusConstantsTracker {
storage_path: PathBuf,
}
impl ConsensusConstantsTracker {
pub fn new(data_dir: &Path) -> Self {
let mut storage_path = data_dir.to_path_buf();
storage_path.push("consensus_constants.json");
Self { storage_path }
}
pub fn load_previous(&self) -> Option<Vec<ConsensusConstants>> {
match fs::read_to_string(&self.storage_path) {
Ok(content) => match serde_json::from_str(&content) {
Ok(constants) => {
debug!(
target: LOG_TARGET,
"Loaded previous consensus constants from {}",
self.storage_path.display()
);
Some(constants)
},
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to parse consensus constants file: {}",
e
);
None
},
},
Err(_) => {
debug!(
target: LOG_TARGET,
"No previous consensus constants file found at {}",
self.storage_path.display()
);
None
},
}
}
pub fn store_current(&self, consensus_constants: &[ConsensusConstants]) -> Result<(), Box<dyn std::error::Error>> {
let content = serde_json::to_string_pretty(consensus_constants)?;
if let Some(parent) = self.storage_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&self.storage_path, content)?;
debug!(
target: LOG_TARGET,
"Stored consensus constants to {}",
self.storage_path.display()
);
Ok(())
}
pub fn check_for_changes(
&self,
current_constants: &[ConsensusConstants],
current_height: u64,
) -> Result<(), String> {
if let Some(previous_constants) = self.load_previous() &&
current_constants != previous_constants
{
info!(
target: LOG_TARGET,
"Consensus constants have changed since last startup"
);
let current_active = current_constants
.iter()
.filter(|cc| cc.effective_from_height() <= current_height)
.max_by_key(|cc| cc.effective_from_height());
let previous_active = previous_constants
.iter()
.filter(|cc| cc.effective_from_height() <= current_height)
.max_by_key(|cc| cc.effective_from_height());
if let (Some(current), Some(previous)) = (current_active, previous_active) &&
current != previous
{
return Err(format!(
"CRITICAL: Consensus constants have changed and the new constants are already active!\nCurrent \
height: {}\nActive consensus constants changed from effective height {} to {}\nThis indicates a \
potential network fork or version mismatch.\nPlease verify you are running the correct version \
of the node for this network.",
current_height,
previous.effective_from_height(),
current.effective_from_height()
));
}
}
if let Err(e) = self.store_current(current_constants) {
warn!(
target: LOG_TARGET,
"Failed to store consensus constants: {}",
e
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use tari_common::configuration::Network;
use tari_transaction_components::consensus::ConsensusConstantsBuilder;
use tempfile::TempDir;
use super::*;
#[test]
fn test_consensus_constants_tracker() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let tracker = ConsensusConstantsTracker::new(temp_dir.path());
let constants1 = vec![ConsensusConstantsBuilder::new(Network::Esmeralda).build()];
let constants2 = vec![ConsensusConstantsBuilder::new(Network::LocalNet).build()];
let result = tracker.check_for_changes(&constants1, 0);
assert!(result.is_ok(), "First run should pass");
assert!(tracker.storage_path.exists(), "Storage file should exist");
let result = tracker.check_for_changes(&constants1, 0);
assert!(result.is_ok(), "Same constants should pass");
let result = tracker.check_for_changes(&constants2, 0);
assert!(result.is_ok(), "Different constants but same active should pass");
}
#[test]
fn test_tracked_consensus_constants_serialization() {
let constants = ConsensusConstantsBuilder::new(Network::Esmeralda).build();
let json = serde_json::to_string(&constants).expect("Should serialize");
let deserialized: ConsensusConstants = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(constants, deserialized);
}
#[test]
fn test_consensus_constants_effective_height_detection() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let tracker = ConsensusConstantsTracker::new(temp_dir.path());
let constants_v1 = vec![ConsensusConstantsBuilder::new(Network::LocalNet).build()];
let constants_v2 = vec![ConsensusConstantsBuilder::new(Network::Esmeralda).build()];
let result = tracker.check_for_changes(&constants_v1, 0);
assert!(result.is_ok(), "First run should pass");
let result = tracker.check_for_changes(&constants_v2, 0);
assert!(
result.is_ok(),
"Should pass when active constants haven't changed at height 0"
);
}
}