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 mut breakpoints: Vec<u64> = current_constants
.iter()
.chain(previous_constants.iter())
.map(|cc| cc.effective_from_height())
.filter(|height| *height <= current_height)
.chain(std::iter::once(current_height))
.collect();
breakpoints.sort_unstable();
breakpoints.dedup();
for height in breakpoints {
let current_active = ConsensusConstants::active_at_height(current_constants, height);
let previous_active = ConsensusConstants::active_at_height(&previous_constants, height);
if let (Some(current), Some(previous)) = (current_active, previous_active) &&
!current.has_same_rules_as(previous)
{
return Err(format!(
"CRITICAL: Consensus constants have changed and the new constants are already \
active!\nCurrent height: {}\nThe rules applying at height {} changed: previously the \
constants effective from height {}, now the constants effective from height {}\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,
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, ConsensusManager};
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 mut constants3 = constants1.clone();
constants3.push(
ConsensusConstantsBuilder::new(Network::Esmeralda)
.with_effective_from_height(1_000_000)
.build(),
);
let result = tracker.check_for_changes(&constants3, 0);
assert!(result.is_ok(), "A not yet effective new entry should pass");
let result = tracker.check_for_changes(&constants2, 0);
assert!(result.is_err(), "Changed active constants should raise the alarm");
}
fn nextnet_constants_before_the_sort_fix() -> Vec<ConsensusConstants> {
let mut previous = ConsensusConstants::for_network(Network::NextNet);
previous.pop();
let con_5 = previous.pop().expect("nextnet has five entries");
assert_eq!(con_5.effective_from_height(), 5_500);
previous.push(
ConsensusConstantsBuilder::new(Network::NextNet)
.with_consensus_constants(con_5)
.with_effective_from_height(5_000)
.build(),
);
previous
}
#[test]
fn upgrading_across_the_nextnet_sort_fix_raises_no_alarm() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let tracker = ConsensusConstantsTracker::new(temp_dir.path());
let previous = nextnet_constants_before_the_sort_fix();
let current = ConsensusConstants::for_network(Network::NextNet);
assert_ne!(
previous, current,
"the vectors must really differ, or this proves nothing"
);
tracker
.store_current(&previous)
.expect("Failed to store previous constants");
for height in [
0, 1_439, 1_440, 1_499, 1_500, 4_999, 5_000, 5_499, 5_500, 5_501, 6_000, 1_000_000,
] {
assert!(
tracker.check_for_changes(¤t, height).is_ok(),
"false fork alarm at height {height}"
);
tracker
.store_current(&previous)
.expect("Failed to restore previous constants");
}
}
#[test]
fn moving_an_activation_height_past_the_tip_raises_the_alarm() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let tracker = ConsensusConstantsTracker::new(temp_dir.path());
let base = ConsensusConstantsBuilder::new(Network::MainNet).build();
let forked = ConsensusConstantsBuilder::new(Network::MainNet)
.with_pow_backoff_cap(32)
.with_difficulty_block_window(45)
.with_effective_from_height(200_000)
.build();
let previous = vec![base.clone(), forked.clone()];
let moved = ConsensusConstantsBuilder::new(Network::MainNet)
.with_consensus_constants(forked)
.with_effective_from_height(210_000)
.build();
let current = vec![base, moved];
tracker.store_current(&previous).expect("Failed to store");
let err = tracker
.check_for_changes(¤t, 220_000)
.expect_err("moving an already passed activation height must raise the alarm");
assert!(
err.contains("The rules applying at height 200000 changed"),
"the message must name the breakpoint where the rules diverge, got: {err}"
);
tracker.store_current(&previous).expect("Failed to store");
assert!(tracker.check_for_changes(¤t, 199_999).is_ok());
}
#[test]
fn the_tracker_agrees_with_the_runtime_lookup() {
for network in [
Network::LocalNet,
Network::Igor,
Network::Esmeralda,
Network::NextNet,
Network::StageNet,
Network::MainNet,
] {
let constants = ConsensusConstants::for_network(network);
let manager = ConsensusManager::builder(network).build();
for height in [0u64, 1, 1_440, 1_500, 5_000, 5_500, 6_000, 126_000, u64::MAX] {
assert_eq!(
ConsensusConstants::active_at_height(&constants, height).expect("never empty"),
manager.consensus_constants(height),
"{network} tracker and runtime disagree at height {height}"
);
}
}
}
#[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 base = ConsensusConstantsBuilder::new(Network::LocalNet).build();
let constants_v1 = vec![base.clone()];
let future_change = ConsensusConstantsBuilder::new(Network::Esmeralda)
.with_effective_from_height(1_000)
.build();
let constants_v2 = vec![base, future_change];
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, 999);
assert!(
result.is_ok(),
"Should pass while the new constants are not yet effective"
);
tracker.store_current(&constants_v1).expect("Failed to store");
let result = tracker.check_for_changes(&constants_v2, 1_000);
assert!(result.is_err(), "Should fail when the new constants are already active");
}
}