use std::path::PathBuf;
use dig_protocol::Bytes32;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SlashingProtection {
last_proposed_slot: u64,
last_attested_source_epoch: u64,
last_attested_target_epoch: u64,
#[serde(default)]
last_attested_block_hash: Option<String>,
}
impl SlashingProtection {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn last_proposed_slot(&self) -> u64 {
self.last_proposed_slot
}
#[must_use]
pub fn check_proposal_slot(&self, slot: u64) -> bool {
slot > self.last_proposed_slot
}
pub fn record_proposal(&mut self, slot: u64) {
self.last_proposed_slot = slot;
}
#[must_use]
pub fn last_attested_source_epoch(&self) -> u64 {
self.last_attested_source_epoch
}
#[must_use]
pub fn last_attested_target_epoch(&self) -> u64 {
self.last_attested_target_epoch
}
#[must_use]
pub fn last_attested_block_hash(&self) -> Option<&str> {
self.last_attested_block_hash.as_deref()
}
#[must_use]
pub fn check_attestation(
&self,
source_epoch: u64,
target_epoch: u64,
block_hash: &Bytes32,
) -> bool {
if self.would_surround(source_epoch, target_epoch) {
return false;
}
if source_epoch == self.last_attested_source_epoch
&& target_epoch == self.last_attested_target_epoch
{
let candidate = to_hex_lower(block_hash.as_ref());
match self.last_attested_block_hash.as_deref() {
Some(stored) if stored.eq_ignore_ascii_case(&candidate) => {
}
_ => return false,
}
}
true
}
#[must_use]
fn would_surround(&self, candidate_source: u64, candidate_target: u64) -> bool {
candidate_source < self.last_attested_source_epoch
&& candidate_target > self.last_attested_target_epoch
}
pub fn record_attestation(
&mut self,
source_epoch: u64,
target_epoch: u64,
block_hash: &Bytes32,
) {
self.last_attested_source_epoch = source_epoch;
self.last_attested_target_epoch = target_epoch;
self.last_attested_block_hash = Some(to_hex_lower(block_hash.as_ref()));
}
pub fn rewind_proposal_to_slot(&mut self, new_tip_slot: u64) {
if self.last_proposed_slot > new_tip_slot {
self.last_proposed_slot = new_tip_slot;
}
}
pub fn save(&self, path: &PathBuf) -> std::io::Result<()> {
let json = serde_json::to_vec_pretty(self)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(path, json)
}
pub fn load(path: &PathBuf) -> std::io::Result<Self> {
if !path.exists() {
return Ok(Self::default());
}
let bytes = std::fs::read(path)?;
serde_json::from_slice(&bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub fn reconcile_with_chain_tip(&mut self, tip_slot: u64, tip_epoch: u64) {
self.rewind_proposal_to_slot(tip_slot);
self.rewind_attestation_to_epoch(tip_epoch);
}
pub fn rewind_attestation_to_epoch(&mut self, new_tip_epoch: u64) {
if self.last_attested_source_epoch > new_tip_epoch {
self.last_attested_source_epoch = new_tip_epoch;
}
if self.last_attested_target_epoch > new_tip_epoch {
self.last_attested_target_epoch = new_tip_epoch;
}
self.last_attested_block_hash = None;
}
}
fn to_hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(2 + bytes.len() * 2);
out.push_str("0x");
for &b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0F) as usize] as char);
}
out
}