use std::collections::BTreeMap;
use bbg::{
Bbg, BoxMove, Cyberlink, IntentRecord, NeuronRecord, PruneConfig, Signal, SignalRecord,
transition::{self, Error, NativeChange},
};
fn graph() -> Bbg {
let mut graph = Bbg::new();
graph.state.neurons.insert(
[1; 32],
NeuronRecord {
focus: 1000,
karma: 4,
stake: 5,
nonce: 0,
},
);
graph.state.refresh_root();
graph.checkpoint = graph.checkpoint.advance(&graph.state);
graph
}
fn signal(amount: u64) -> Signal {
Signal {
neuron: [1; 32],
links: vec![Cyberlink {
from: [2; 32],
to: [3; 32],
token: [0; 32],
amount,
valence: 1,
}],
box_moves: vec![BoxMove {
nullifier: [7; 32],
commitment: Some(([8; 32], 23)),
}],
height: 0,
}
}
fn header(signal: &Signal, step: u64) -> SignalRecord {
SignalRecord {
neuron: signal.neuron,
network: [55; 32],
link_count: signal.links.len() as u32,
block_height: signal.height,
proof_hash: [99; 32],
step,
}
}
fn records(graph: &Bbg) -> BTreeMap<Vec<u8>, Vec<u8>> {
transition::records(graph)
.unwrap()
.map(|r| (r.key, r.value))
.collect()
}
#[test]
fn dropped_preparation_restores_every_record_and_checkpoint() {
let mut graph = graph();
let before = records(&graph);
let signal = signal(13);
let old_root = graph.state.root();
{
let prepared = graph
.prepare_native(Some(&signal), Some(header(&signal, 256)), None)
.unwrap();
assert_eq!(prepared.height(), 1);
assert_ne!(prepared.root(), old_root);
assert!(!prepared.changes().is_empty());
}
assert_eq!(records(&graph), before);
assert_eq!(graph.state.root(), old_root);
}
#[test]
fn callback_failure_rolls_back_and_later_retry_succeeds() {
let mut graph = graph();
let before = records(&graph);
let signal = signal(13);
fn attempt(
graph: &mut Bbg,
signal: &Signal,
commit: impl FnOnce(&[transition::RecordChange]) -> Result<(), &'static str>,
) -> Result<(), &'static str> {
let prepared = graph
.prepare_native(Some(signal), Some(header(signal, 0)), None)
.unwrap();
commit(prepared.changes())?;
prepared.publish();
Ok(())
}
let failed = attempt(&mut graph, &signal, |_| Err("durable transaction rejected"));
assert!(failed.is_err());
assert_eq!(records(&graph), before);
graph
.prepare_native(Some(&signal), Some(header(&signal, 0)), None)
.unwrap()
.publish();
assert_eq!(graph.state.height, 1);
assert!(graph.state.nullifiers.contains(&[7; 32]));
}
#[test]
fn published_candidate_matches_standard_signal_finalization_and_exact_changes() {
let mut candidate = graph();
let mut standard = graph();
let signal = signal(19);
let mut disk = records(&candidate);
standard.insert(&signal).unwrap();
standard.apply_signal_record(header(&signal, 9));
standard.finalize_block();
let prepared = candidate
.prepare_native(Some(&signal), Some(header(&signal, 9)), None)
.unwrap();
for change in prepared.changes() {
if let Some(value) = &change.value {
disk.insert(change.key.clone(), value.clone());
} else {
disk.remove(&change.key);
}
}
assert_eq!(prepared.root(), standard.state.root());
prepared.publish();
assert_eq!(records(&candidate), disk);
assert_eq!(records(&candidate), records(&standard));
}
#[test]
fn duplicate_nullifier_inside_one_signal_is_rejected_before_mutation() {
let mut graph = graph();
let before = records(&graph);
let mut signal = signal(2);
signal.box_moves.push(BoxMove {
nullifier: [7; 32],
commitment: None,
});
assert!(matches!(
graph.prepare_native(Some(&signal), Some(header(&signal, 0)), None),
Err(Error::DoubleSpend)
));
assert_eq!(records(&graph), before);
}
#[test]
fn overflow_is_rejected_even_when_credit_and_debit_keys_alias() {
let mut graph = graph();
let mut signal = signal(1);
signal.links[0].from = signal.links[0].to;
graph
.state
.balances
.insert(bbg::balance_key(&[3; 32], &[0; 32]), u64::MAX);
graph.state.refresh_root();
let before = records(&graph);
assert!(matches!(
graph.prepare_native(Some(&signal), Some(header(&signal, 0)), None),
Err(Error::Overflow("balance"))
));
assert_eq!(records(&graph), before);
graph.state.height = u64::MAX;
assert!(matches!(
graph.prepare_native(Some(&signal), Some(header(&signal, 0)), None),
Err(Error::Overflow("height"))
));
assert_eq!(graph.state.height, u64::MAX);
}
#[test]
fn failure_in_later_batch_event_rolls_back_earlier_signal_and_intent() {
let mut graph = graph();
let before = records(&graph);
let first = signal(2);
let second = signal(3); let first_header = header(&first, 0);
let second_header = header(&second, 1);
let intent = IntentRecord {
neuron: [1; 32],
h0: 0,
scope_hash: [5; 32],
signature: [6; 64],
};
let batch = [
NativeChange::Signal {
signal: &first,
header: &first_header,
},
NativeChange::Intent(&intent),
NativeChange::Signal {
signal: &second,
header: &second_header,
},
];
assert!(matches!(
graph.prepare_native_batch(&batch),
Err(Error::DoubleSpend)
));
assert_eq!(records(&graph), before);
}
#[test]
fn epoch_pruning_restores_removed_records_and_adjacency_on_drop() {
let mut graph = graph().with_prune_config(PruneConfig {
max_bytes: 0,
rank_floor_pct: 0,
half_life_epochs: 0,
});
let mut old_signal = signal(9);
old_signal.box_moves.clear();
graph.insert(&old_signal).unwrap();
graph.state.height = 99;
graph.state.refresh_root();
graph.checkpoint = graph.checkpoint.advance(&graph.state);
let before = records(&graph);
let next = signal(3);
let prepared = graph
.prepare_native(Some(&next), Some(header(&next, 7)), None)
.unwrap();
assert_eq!(prepared.height(), 100);
assert!(
prepared
.changes()
.iter()
.any(|change| change.value.is_none())
);
drop(prepared);
assert_eq!(records(&graph), before);
graph
.prepare_native(Some(&next), Some(header(&next, 7)), None)
.unwrap()
.publish();
assert!(graph.state.particles.is_empty());
assert!(graph.state.axons_out.is_empty());
assert_eq!(graph.state.root(), graph.state.compute_root());
}
#[test]
fn intent_only_persists_exact_record_and_retains_current_root_and_height() {
let mut graph = graph();
let before = graph.state.root();
let intent = IntentRecord {
neuron: [1; 32],
h0: u64::MAX,
scope_hash: [3; 32],
signature: [4; 64],
};
let prepared = graph.prepare_native(None, None, Some(&intent)).unwrap();
assert_eq!(prepared.height(), 0);
assert_eq!(prepared.root(), before);
assert_eq!(prepared.changes().len(), 1);
assert_eq!(prepared.changes()[0].key[0], 14);
assert_eq!(
&prepared.changes()[0].value.as_ref().unwrap()[32..40],
&u64::MAX.to_le_bytes()
);
prepared.publish();
assert_eq!(graph.state.intents.len(), 1);
}
#[test]
fn typed_records_preserve_raw_integer_bytes_and_sort_numeric_keys() {
let mut graph = graph();
graph.state.time.insert(255, [1; 32]);
graph.state.time.insert(256, [2; 32]);
graph.state.balances.insert([0xff; 32], u64::MAX);
let records: Vec<_> = transition::records(&graph).unwrap().collect();
assert!(records.windows(2).all(|pair| pair[0].key < pair[1].key));
let balance = records
.iter()
.find(|r| r.key == [vec![13], vec![0xff; 32]].concat())
.unwrap();
assert_eq!(balance.value, u64::MAX.to_le_bytes());
let time: Vec<_> = records.iter().filter(|r| r.key[0] == 9).collect();
assert_eq!(time[0].value, [1; 32]);
assert_eq!(time[1].value, [2; 32]);
}
#[test]
fn preparation_touches_only_signal_neighborhood_in_large_graph() {
let mut graph = graph();
for n in 0..1000u64 {
let mut key = [0; 32];
key[..8].copy_from_slice(&n.to_le_bytes());
graph.state.balances.insert(key, n);
}
graph.state.refresh_root();
let signal = signal(2);
let prepared = graph
.prepare_native(Some(&signal), Some(header(&signal, 0)), None)
.unwrap();
assert!(prepared.changes().len() < 20);
}
#[test]
fn repeated_neuron_steps_and_inconsistent_headers_are_rejected() {
let mut graph = graph();
let mut signal = signal(1);
signal.box_moves.clear();
graph
.prepare_native(Some(&signal), Some(header(&signal, 123)), None)
.unwrap()
.publish();
let before = records(&graph);
assert!(matches!(
graph.prepare_native(Some(&signal), Some(header(&signal, 123)), None),
Err(Error::SignalExists)
));
let mut wrong = header(&signal, 124);
wrong.neuron = [0; 32];
assert!(matches!(
graph.prepare_native(Some(&signal), Some(wrong), None),
Err(Error::Arguments)
));
assert_eq!(records(&graph), before);
}
#[test]
fn successful_batch_coalesces_overlapping_keys_and_keeps_each_snapshot() {
let mut graph = graph();
let mut baseline = self::graph();
let mut disk = records(&graph);
let first = signal(3);
let mut second = signal(5);
second.box_moves[0].nullifier = [9; 32];
let first_header = header(&first, 0);
let second_header = header(&second, 1);
let intent = IntentRecord {
neuron: [1; 32],
h0: 1,
scope_hash: [5; 32],
signature: [6; 64],
};
baseline.insert(&first).unwrap();
baseline.apply_signal_record(header(&first, 0));
baseline.finalize_block();
let first_root = baseline.state.root();
baseline.apply_intent(&intent);
baseline.insert(&second).unwrap();
baseline.apply_signal_record(header(&second, 1));
baseline.finalize_block();
let prepared = graph
.prepare_native_batch(&[
NativeChange::Signal {
signal: &first,
header: &first_header,
},
NativeChange::Intent(&intent),
NativeChange::Signal {
signal: &second,
header: &second_header,
},
])
.unwrap();
assert_eq!(
prepared
.snapshots()
.iter()
.map(|s| s.height)
.collect::<Vec<_>>(),
vec![1, 1, 2]
);
assert_eq!(prepared.snapshots()[0].root, first_root);
assert_eq!(prepared.snapshots()[1].root, first_root);
assert_eq!(prepared.snapshots()[2].root, baseline.state.root());
for change in prepared.changes() {
if let Some(value) = &change.value {
disk.insert(change.key.clone(), value.clone());
} else {
disk.remove(&change.key);
}
}
prepared.publish();
assert_eq!(records(&graph), disk);
assert_eq!(records(&graph), records(&baseline));
}
#[test]
fn empty_and_oversized_batches_fail_without_changing_state() {
let mut graph = graph();
let before = records(&graph);
assert!(matches!(
graph.prepare_native_batch(&[]),
Err(Error::Limit("event count"))
));
let intent = IntentRecord {
neuron: [1; 32],
h0: 0,
scope_hash: [5; 32],
signature: [6; 64],
};
let batch: Vec<_> = (0..65).map(|_| NativeChange::Intent(&intent)).collect();
assert!(matches!(
graph.prepare_native_batch(&batch),
Err(Error::Limit("event count"))
));
assert_eq!(records(&graph), before);
}