use sha2::{Digest, Sha256};
pub fn bft_quorum(_n: u32, f: u32) -> u32 {
2 * f + 1
}
pub fn bft_max_faults(n: u32) -> u32 {
if n == 0 {
return 0;
}
(n - 1) / 3
}
pub fn bft_check(votes: u32, f: u32) -> bool {
votes >= bft_quorum(0, f)
}
#[derive(Debug, Clone)]
pub struct ChainedEvent {
pub chain_hash: [u8; 32],
pub event_bytes: Vec<u8>,
pub prev_hash: [u8; 32],
}
pub fn hash_chain_extend(prev_hash: &[u8; 32], event_bytes: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(prev_hash);
hasher.update(event_bytes);
hasher.finalize().into()
}
pub fn verify_chain(events: &[ChainedEvent]) -> bool {
for event in events {
let expected = hash_chain_extend(&event.prev_hash, &event.event_bytes);
if expected != event.chain_hash {
return false;
}
}
true
}
#[cfg(feature = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn bft_quorum_wasm(n: u32, f: u32) -> u32 {
bft_quorum(n, f)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hash_chain_extend_wasm(prev_hash_hex: &str, event_json: &str) -> String {
let Ok(prev_bytes) = hex::decode(prev_hash_hex) else {
return r#"{"error":"invalid prev_hash_hex"}"#.to_string();
};
let Ok(prev_arr): Result<[u8; 32], _> = prev_bytes.try_into() else {
return r#"{"error":"prev_hash must be 32 bytes"}"#.to_string();
};
let hash = hash_chain_extend(&prev_arr, event_json.as_bytes());
hex::encode(hash)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bft_quorum_basic() {
assert_eq!(bft_quorum(1, 0), 1);
assert_eq!(bft_quorum(4, 1), 3);
assert_eq!(bft_quorum(7, 2), 5);
assert_eq!(bft_quorum(10, 3), 7);
}
#[test]
fn test_bft_max_faults() {
assert_eq!(bft_max_faults(0), 0);
assert_eq!(bft_max_faults(1), 0);
assert_eq!(bft_max_faults(3), 0); assert_eq!(bft_max_faults(4), 1);
assert_eq!(bft_max_faults(7), 2);
assert_eq!(bft_max_faults(10), 3);
}
#[test]
fn test_bft_check() {
assert!(bft_check(3, 1));
assert!(bft_check(4, 1));
assert!(!bft_check(2, 1));
assert!(!bft_check(1, 1));
assert!(bft_check(5, 2));
assert!(!bft_check(4, 2));
assert!(bft_check(1, 0));
assert!(!bft_check(0, 0));
}
#[test]
fn test_hash_chain_extend_deterministic() {
let prev = [0u8; 32];
let h1 = hash_chain_extend(&prev, b"event one");
let h2 = hash_chain_extend(&prev, b"event one");
assert_eq!(h1, h2, "same inputs must produce same hash");
}
#[test]
fn test_hash_chain_extend_unique() {
let prev = [0u8; 32];
let h1 = hash_chain_extend(&prev, b"event one");
let h2 = hash_chain_extend(&prev, b"event two");
assert_ne!(h1, h2);
}
#[test]
fn test_hash_chain_extend_chaining() {
let prev = [0u8; 32];
let h1 = hash_chain_extend(&prev, b"event one");
let h2 = hash_chain_extend(&h1, b"event two");
let h2_swapped = hash_chain_extend(&prev, b"event two");
assert_ne!(h2, h2_swapped);
}
#[test]
fn test_verify_chain_valid() {
let prev = [0u8; 32];
let bytes1 = b"approval:agent-1:APPROVED".to_vec();
let h1 = hash_chain_extend(&prev, &bytes1);
let bytes2 = b"approval:agent-2:APPROVED".to_vec();
let h2 = hash_chain_extend(&h1, &bytes2);
let bytes3 = b"approval:agent-3:APPROVED".to_vec();
let h3 = hash_chain_extend(&h2, &bytes3);
let chain = vec![
ChainedEvent {
chain_hash: h1,
event_bytes: bytes1,
prev_hash: prev,
},
ChainedEvent {
chain_hash: h2,
event_bytes: bytes2,
prev_hash: h1,
},
ChainedEvent {
chain_hash: h3,
event_bytes: bytes3,
prev_hash: h2,
},
];
assert!(verify_chain(&chain), "valid chain must verify");
}
#[test]
fn test_verify_chain_tampered_payload() {
let prev = [0u8; 32];
let bytes1 = b"approval:agent-1:APPROVED".to_vec();
let h1 = hash_chain_extend(&prev, &bytes1);
let mut chain = vec![ChainedEvent {
chain_hash: h1,
event_bytes: bytes1,
prev_hash: prev,
}];
chain[0].event_bytes = b"approval:agent-1:REJECTED".to_vec();
assert!(
!verify_chain(&chain),
"tampered payload must fail verification"
);
}
#[test]
fn test_verify_chain_tampered_hash() {
let prev = [0u8; 32];
let bytes1 = b"approval:agent-1:APPROVED".to_vec();
let h1 = hash_chain_extend(&prev, &bytes1);
let mut chain = vec![ChainedEvent {
chain_hash: h1,
event_bytes: bytes1,
prev_hash: prev,
}];
chain[0].chain_hash[0] ^= 0xFF;
assert!(
!verify_chain(&chain),
"tampered chain_hash must fail verification"
);
}
#[test]
fn test_verify_chain_empty() {
assert!(verify_chain(&[]));
}
#[test]
fn test_bft_adversarial_3honest_2byzantine() {
let f = 1u32;
let honest_votes = 3u32;
let byzantine_votes = 2u32;
assert!(bft_check(honest_votes, f), "honest quorum must be reached");
assert!(
!bft_check(byzantine_votes, f),
"byzantine faction must NOT reach quorum"
);
}
#[test]
fn test_bft_default_config_f1_quorum3() {
let f = 1u32;
let quorum = bft_quorum(0, f);
assert_eq!(quorum, 3);
assert!(bft_check(3, f));
assert!(!bft_check(2, f));
}
#[test]
fn test_hash_chain_10_sequential() {
let mut prev = [0u8; 32];
let mut chain = Vec::new();
for i in 0u32..10 {
let event_bytes = format!("approval:agent-{i}:APPROVED").into_bytes();
let chain_hash = hash_chain_extend(&prev, &event_bytes);
chain.push(ChainedEvent {
chain_hash,
event_bytes,
prev_hash: prev,
});
prev = chain_hash;
}
assert_eq!(chain.len(), 10);
assert!(verify_chain(&chain), "10-event chain must verify");
chain[5].event_bytes = b"TAMPERED".to_vec();
assert!(!verify_chain(&chain), "tampered event must fail");
}
}