use std::sync::Arc;
use dashmap::DashMap;
use freenet_stdlib::prelude::ContractInstanceId;
use crate::contract::storages::Storage;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrokenInvariant {
NonIdempotent,
}
impl BrokenInvariant {
fn to_byte(self) -> u8 {
match self {
BrokenInvariant::NonIdempotent => 0,
}
}
fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(BrokenInvariant::NonIdempotent),
_ => None,
}
}
}
#[derive(Default)]
pub(crate) struct BrokenInvariantsTracker {
flags: Arc<DashMap<ContractInstanceId, BrokenInvariant>>,
storage: std::sync::OnceLock<Storage>,
}
impl BrokenInvariantsTracker {
pub fn new() -> Self {
Self::default()
}
pub fn is_broken(&self, id: &ContractInstanceId) -> bool {
self.flags.contains_key(id)
}
#[cfg(test)]
pub fn get(&self, id: &ContractInstanceId) -> Option<BrokenInvariant> {
self.flags.get(id).map(|r| *r.value())
}
pub fn record(&self, id: ContractInstanceId, kind: BrokenInvariant) {
let was_new = self.flags.insert(id, kind).is_none();
if was_new {
tracing::warn!(
contract = %id,
invariant = ?kind,
event = "broken_invariant_detected",
"Marking contract as broken — gating outbound broadcast and merge propagation"
);
#[cfg(feature = "redb")]
if let Some(storage) = self.storage.get() {
if let Err(e) = storage.store_broken_invariant(&id, kind.to_byte()) {
tracing::warn!(
contract = %id,
error = %e,
"Failed to persist broken-invariant flag (in-memory flag still active)"
);
}
}
}
}
#[allow(dead_code)] pub fn clear(&self, id: &ContractInstanceId) -> Option<BrokenInvariant> {
let previous = self.flags.remove(id).map(|(_, v)| v);
if previous.is_some() {
#[cfg(feature = "redb")]
if let Some(storage) = self.storage.get() {
if let Err(e) = storage.remove_broken_invariant(id) {
tracing::warn!(
contract = %id,
error = %e,
"Cleared in-memory broken-invariant flag, but persistence remove failed — \
flag will be re-loaded on next restart"
);
}
}
tracing::warn!(
contract = %id,
event = "broken_invariant_cleared",
"Operator cleared broken-invariant flag — outbound broadcast re-enabled"
);
}
previous
}
pub fn set_storage(&self, storage: Storage) {
if self.storage.set(storage.clone()).is_err() {
tracing::warn!("BrokenInvariantsTracker storage already set; ignoring re-init");
return;
}
#[cfg(feature = "redb")]
match storage.load_all_broken_invariants() {
Ok(entries) => {
for (id, byte) in entries {
if let Some(kind) = BrokenInvariant::from_byte(byte) {
self.flags.insert(id, kind);
} else {
tracing::warn!(
contract = %id,
byte,
"Skipping unknown broken-invariant byte on load"
);
}
}
tracing::debug!(
count = self.flags.len(),
"Loaded broken-invariant flags from storage"
);
}
Err(e) => {
tracing::warn!(error = %e, "Failed to load broken-invariant flags from storage");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_id(seed: u8) -> ContractInstanceId {
let mut bytes = [0u8; 32];
bytes[0] = seed;
ContractInstanceId::new(bytes)
}
#[test]
fn record_then_query_returns_true() {
let t = BrokenInvariantsTracker::new();
let id = fake_id(1);
assert!(!t.is_broken(&id));
t.record(id, BrokenInvariant::NonIdempotent);
assert!(t.is_broken(&id));
assert_eq!(t.get(&id), Some(BrokenInvariant::NonIdempotent));
}
#[test]
fn record_is_idempotent() {
let t = BrokenInvariantsTracker::new();
let id = fake_id(2);
t.record(id, BrokenInvariant::NonIdempotent);
t.record(id, BrokenInvariant::NonIdempotent);
assert!(t.is_broken(&id));
}
#[test]
fn unrelated_contracts_unaffected() {
let t = BrokenInvariantsTracker::new();
let broken = fake_id(3);
let healthy = fake_id(4);
t.record(broken, BrokenInvariant::NonIdempotent);
assert!(t.is_broken(&broken));
assert!(!t.is_broken(&healthy));
}
#[test]
fn clear_returns_previous_and_unsets() {
let t = BrokenInvariantsTracker::new();
let id = fake_id(5);
assert_eq!(t.clear(&id), None);
t.record(id, BrokenInvariant::NonIdempotent);
assert!(t.is_broken(&id));
let prev = t.clear(&id);
assert_eq!(prev, Some(BrokenInvariant::NonIdempotent));
assert!(
!t.is_broken(&id),
"after clear the contract is no longer broken"
);
assert_eq!(t.clear(&id), None);
}
#[test]
fn byte_roundtrip_stable() {
let kinds: &[BrokenInvariant] = &[BrokenInvariant::NonIdempotent];
for kind in kinds {
assert_eq!(BrokenInvariant::from_byte(kind.to_byte()), Some(*kind));
}
assert_eq!(BrokenInvariant::from_byte(255), None);
}
}