use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::collections::BTreeSet;
use std::sync::{Mutex, MutexGuard, OnceLock};
use lens::Commitment;
use nebu::Goldilocks;
use crate::dim::{bytes32_limbs, u64_limbs};
use crate::trie::{leaf_digest, Digest, Trie, TrieProof};
use crate::types::Particle;
pub trait TrieKey: Ord + Copy {
fn key_bytes(&self) -> [u8; 32];
}
impl TrieKey for [u8; 32] {
fn key_bytes(&self) -> [u8; 32] {
*self
}
}
impl TrieKey for u64 {
fn key_bytes(&self) -> [u8; 32] {
let mut key = [0u8; 32];
key[..8].copy_from_slice(&self.to_le_bytes());
key
}
}
pub trait LeafValue {
fn leaf_vals(&self) -> Vec<Goldilocks>;
}
impl LeafValue for u64 {
fn leaf_vals(&self) -> Vec<Goldilocks> {
u64_limbs(*self).to_vec()
}
}
impl LeafValue for Goldilocks {
fn leaf_vals(&self) -> Vec<Goldilocks> {
vec![*self]
}
}
impl LeafValue for [u8; 32] {
fn leaf_vals(&self) -> Vec<Goldilocks> {
bytes32_limbs(self).to_vec()
}
}
impl LeafValue for () {
fn leaf_vals(&self) -> Vec<Goldilocks> {
Vec::new()
}
}
pub struct Keyed<K, V> {
map: BTreeMap<K, V>,
commit: Mutex<Pending<K>>,
legacy: OnceLock<Commitment>,
}
struct Pending<K> {
trie: Trie,
dirty: BTreeSet<K>,
}
impl<K> Default for Pending<K> {
fn default() -> Self {
Self { trie: Trie::new(), dirty: BTreeSet::new() }
}
}
impl<K: TrieKey, V: LeafValue> Default for Keyed<K, V> {
fn default() -> Self {
Self { map: BTreeMap::new(), commit: Mutex::new(Pending::default()), legacy: OnceLock::new() }
}
}
impl<K, V> Deref for Keyed<K, V> {
type Target = BTreeMap<K, V>;
fn deref(&self) -> &BTreeMap<K, V> {
&self.map
}
}
impl<K: TrieKey, V: LeafValue> Keyed<K, V> {
pub fn new() -> Self {
Self::default()
}
fn touched(&mut self, key: &K) {
self.legacy = OnceLock::new();
self.commit.get_mut().expect("commitment lock").dirty.insert(*key);
}
fn flush(&self) -> MutexGuard<'_, Pending<K>> {
let mut pending = self.commit.lock().expect("commitment lock");
let Pending { trie, dirty } = &mut *pending;
for key in std::mem::take(dirty) {
match self.map.get(&key) {
Some(v) => {
trie.set(key.key_bytes(), leaf_digest(&key.key_bytes(), &v.leaf_vals()));
}
None => {
trie.remove(&key.key_bytes());
}
}
}
pending
}
pub fn pending(&self) -> usize {
self.commit.lock().expect("commitment lock").dirty.len()
}
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
let old = self.map.insert(key, value);
self.touched(&key);
old
}
pub fn remove(&mut self, key: &K) -> Option<V> {
let old = self.map.remove(key)?;
self.touched(key);
Some(old)
}
pub fn clear(&mut self) {
self.map.clear();
*self.commit.get_mut().expect("commitment lock") = Pending::default();
self.legacy = OnceLock::new();
}
pub fn get_mut(&mut self, key: &K) -> Option<ValueMut<'_, K, V>> {
if !self.map.contains_key(key) {
return None;
}
Some(ValueMut { keyed: self, key: *key })
}
pub fn entry(&mut self, key: K) -> Entry<'_, K, V> {
Entry { keyed: self, key }
}
pub fn commitment(&self) -> Digest {
self.flush().trie.root()
}
pub fn legacy_commitment(&self, f: impl FnOnce(&BTreeMap<K, V>) -> Commitment) -> Commitment {
*self.legacy.get_or_init(|| f(&self.map))
}
pub fn prove(&self, key: &K) -> TrieProof {
self.flush().trie.prove(&key.key_bytes())
}
pub fn leaf(key: &K, value: &V) -> Digest {
leaf_digest(&key.key_bytes(), &value.leaf_vals())
}
}
pub struct ValueMut<'a, K: TrieKey, V: LeafValue> {
keyed: &'a mut Keyed<K, V>,
key: K,
}
impl<K: TrieKey, V: LeafValue> Deref for ValueMut<'_, K, V> {
type Target = V;
fn deref(&self) -> &V {
self.keyed.map.get(&self.key).expect("guarded key is present")
}
}
impl<K: TrieKey, V: LeafValue> DerefMut for ValueMut<'_, K, V> {
fn deref_mut(&mut self) -> &mut V {
self.keyed.map.get_mut(&self.key).expect("guarded key is present")
}
}
impl<K: TrieKey, V: LeafValue> Drop for ValueMut<'_, K, V> {
fn drop(&mut self) {
let key = self.key;
self.keyed.touched(&key);
}
}
pub struct Entry<'a, K: TrieKey, V: LeafValue> {
keyed: &'a mut Keyed<K, V>,
key: K,
}
impl<'a, K: TrieKey, V: LeafValue> Entry<'a, K, V> {
pub fn or_insert(self, default: V) -> ValueMut<'a, K, V> {
self.or_insert_with(|| default)
}
pub fn or_insert_with(self, default: impl FnOnce() -> V) -> ValueMut<'a, K, V> {
if !self.keyed.map.contains_key(&self.key) {
self.keyed.map.insert(self.key, default());
}
ValueMut { keyed: self.keyed, key: self.key }
}
pub fn or_default(self) -> ValueMut<'a, K, V>
where
V: Default,
{
self.or_insert_with(V::default)
}
}
#[derive(Default)]
pub struct KeyedSet<K: TrieKey> {
inner: Keyed<K, ()>,
}
impl<K: TrieKey> KeyedSet<K> {
pub fn new() -> Self {
Self { inner: Keyed::new() }
}
pub fn insert(&mut self, key: K) -> bool {
self.inner.insert(key, ()).is_none()
}
pub fn remove(&mut self, key: &K) -> bool {
self.inner.remove(key).is_some()
}
pub fn contains(&self, key: &K) -> bool {
self.inner.contains_key(key)
}
pub fn iter(&self) -> impl Iterator<Item = &K> {
self.inner.keys()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn commitment(&self) -> Digest {
self.inner.commitment()
}
pub fn pending(&self) -> usize {
self.inner.pending()
}
pub fn legacy_commitment(&self, f: impl FnOnce(&BTreeMap<K, ()>) -> Commitment) -> Commitment {
self.inner.legacy_commitment(f)
}
pub fn prove(&self, key: &K) -> TrieProof {
self.inner.prove(key)
}
}
pub struct AxonIndex {
map: BTreeMap<Particle, Vec<Particle>>,
degrees: BTreeMap<usize, usize>,
commit: Mutex<AxonPending>,
legacy: OnceLock<Commitment>,
}
#[derive(Default)]
struct AxonPending {
sets: BTreeMap<Particle, Trie>,
trie: Trie,
dirty: BTreeSet<Particle>,
}
impl Default for AxonIndex {
fn default() -> Self {
Self {
map: BTreeMap::new(),
degrees: BTreeMap::new(),
commit: Mutex::new(AxonPending::default()),
legacy: OnceLock::new(),
}
}
}
impl Deref for AxonIndex {
type Target = BTreeMap<Particle, Vec<Particle>>;
fn deref(&self) -> &BTreeMap<Particle, Vec<Particle>> {
&self.map
}
}
impl AxonIndex {
pub fn new() -> Self {
Self::default()
}
fn member_digest(axon: &Particle) -> Digest {
leaf_digest(axon, &[])
}
pub fn leaf(particle: &Particle, degree: u64, set_root: &Digest) -> Digest {
let mut vals = u64_limbs(degree).to_vec();
vals.extend_from_slice(set_root);
leaf_digest(particle, &vals)
}
fn degree_moved(&mut self, old: usize, new: usize) {
if old == new {
return;
}
if old > 0 {
let n = self.degrees.get_mut(&old).expect("histogram tracks every list");
*n -= 1;
if *n == 0 {
self.degrees.remove(&old);
}
}
if new > 0 {
*self.degrees.entry(new).or_insert(0) += 1;
}
}
fn degree(&self, particle: &Particle) -> usize {
self.map.get(particle).map_or(0, Vec::len)
}
fn pending_mut(&mut self) -> &mut AxonPending {
self.commit.get_mut().expect("commitment lock")
}
fn touched(&mut self, particle: &Particle) {
self.legacy = OnceLock::new();
self.pending_mut().dirty.insert(*particle);
}
fn flush(&self) -> MutexGuard<'_, AxonPending> {
let mut pending = self.commit.lock().expect("commitment lock");
let AxonPending { sets, trie, dirty } = &mut *pending;
for particle in std::mem::take(dirty) {
match (self.map.get(&particle), sets.get_mut(&particle)) {
(Some(list), Some(set)) => {
let digest = Self::leaf(&particle, list.len() as u64, &set.root());
trie.set(particle, digest);
}
_ => {
trie.remove(&particle);
}
}
}
pending
}
pub fn pending(&self) -> usize {
self.commit.lock().expect("commitment lock").dirty.len()
}
pub fn add(&mut self, particle: Particle, axon: Particle) -> bool {
let set = self.pending_mut().sets.entry(particle).or_default();
if !set.set(axon, Self::member_digest(&axon)) {
return false;
}
let list = self.map.entry(particle).or_default();
list.push(axon);
let len = list.len();
self.degree_moved(len - 1, len);
self.touched(&particle);
true
}
pub fn insert(&mut self, particle: Particle, list: Vec<Particle>) -> Option<Vec<Particle>> {
let old = self.remove(&particle);
let mut set = Trie::new();
let list: Vec<Particle> = list.into_iter().filter(|a| set.set(*a, Self::member_digest(a))).collect();
if list.is_empty() {
return old;
}
self.degree_moved(0, list.len());
self.map.insert(particle, list);
self.pending_mut().sets.insert(particle, set);
self.touched(&particle);
old
}
pub fn remove(&mut self, particle: &Particle) -> Option<Vec<Particle>> {
let old = self.map.remove(particle)?;
self.pending_mut().sets.remove(particle);
self.degree_moved(old.len(), 0);
self.touched(particle);
Some(old)
}
pub fn remove_axon(&mut self, particle: &Particle, axon: &Particle) -> bool {
let Some(set) = self.pending_mut().sets.get_mut(particle) else { return false };
if !set.remove(axon) {
return false;
}
let list = self.map.get_mut(particle).expect("list mirrors set");
let before = list.len();
list.retain(|a| a != axon);
let after = list.len();
if after == 0 {
self.map.remove(particle);
self.pending_mut().sets.remove(particle);
}
self.degree_moved(before, after);
self.touched(particle);
true
}
pub fn truncate(&mut self, particle: &Particle, len: usize) -> usize {
let before = self.degree(particle);
if before <= len {
return 0;
}
if len == 0 {
return self.remove(particle).map_or(0, |l| l.len());
}
let dropped = self.map.get_mut(particle).expect("degree > 0").split_off(len);
let set = self.pending_mut().sets.get_mut(particle).expect("list mirrors set");
for axon in &dropped {
set.remove(axon);
}
self.degree_moved(before, len);
self.touched(particle);
dropped.len()
}
pub fn clear(&mut self) {
self.map.clear();
*self.pending_mut() = AxonPending::default();
self.degrees.clear();
self.legacy = OnceLock::new();
}
pub fn max_degree(&self) -> usize {
self.degrees.last_key_value().map_or(0, |(d, _)| *d)
}
pub fn commitment(&self) -> Digest {
self.flush().trie.root()
}
pub fn set_root(&self, particle: &Particle) -> Option<Digest> {
self.commit.lock().expect("commitment lock").sets.get_mut(particle).map(Trie::root)
}
pub fn legacy_commitment(
&self,
f: impl FnOnce(&BTreeMap<Particle, Vec<Particle>>) -> Commitment,
) -> Commitment {
*self.legacy.get_or_init(|| f(&self.map))
}
pub fn prove(&self, particle: &Particle) -> TrieProof {
self.flush().trie.prove(particle)
}
pub fn prove_member(&self, particle: &Particle, axon: &Particle) -> Option<TrieProof> {
self.commit.lock().expect("commitment lock").sets.get_mut(particle).map(|s| s.prove(axon))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::trie::EMPTY;
fn p(seed: u8) -> Particle {
[seed; 32]
}
#[test]
fn every_mutation_path_updates_the_trie() {
let mut m: Keyed<[u8; 32], u64> = Keyed::new();
m.insert(p(1), 10);
let expected = |entries: &[([u8; 32], u64)]| {
Trie::from_entries(entries.iter().map(|(k, v)| (*k, Keyed::<[u8; 32], u64>::leaf(k, v)))).root()
};
assert_eq!(m.commitment(), expected(&[(p(1), 10)]));
*m.get_mut(&p(1)).unwrap() += 5;
assert_eq!(m.commitment(), expected(&[(p(1), 15)]));
*m.entry(p(2)).or_insert(0) += 7;
assert_eq!(m.commitment(), expected(&[(p(1), 15), (p(2), 7)]));
*m.entry(p(1)).or_insert(999) = 1;
assert_eq!(m.commitment(), expected(&[(p(1), 1), (p(2), 7)]));
m.remove(&p(2));
assert_eq!(m.commitment(), expected(&[(p(1), 1)]));
m.remove(&p(1));
assert_eq!(m.commitment(), EMPTY);
assert!(m.get_mut(&p(3)).is_none());
m.insert(p(4), 4);
m.clear();
assert_eq!(m.commitment(), EMPTY);
assert!(m.is_empty());
}
#[test]
fn legacy_cache_resets_on_mutation() {
let mut m: Keyed<[u8; 32], u64> = Keyed::new();
let count = std::cell::Cell::new(0);
let f = |map: &BTreeMap<[u8; 32], u64>| {
count.set(count.get() + 1);
Commitment(hemera::hash(&(map.len() as u64).to_le_bytes()))
};
m.insert(p(1), 1);
let a = m.legacy_commitment(f);
let b = m.legacy_commitment(f);
assert_eq!(a, b);
assert_eq!(count.get(), 1, "cached between mutations");
m.insert(p(2), 2);
let c = m.legacy_commitment(f);
assert_ne!(a, c);
assert_eq!(count.get(), 2, "recomputed after a mutation");
}
#[test]
fn axon_index_is_a_set_with_a_nested_root() {
let mut ix = AxonIndex::new();
assert!(ix.add(p(1), p(10)));
assert!(ix.add(p(1), p(11)));
assert!(!ix.add(p(1), p(10)), "duplicates are ignored");
assert_eq!(ix.get(&p(1)).unwrap().len(), 2);
assert_eq!(ix.max_degree(), 2);
let set = Trie::from_entries([p(10), p(11)].iter().map(|a| (*a, AxonIndex::member_digest(a)))).root();
let outer = Trie::from_entries([(p(1), AxonIndex::leaf(&p(1), 2, &set))]).root();
assert_eq!(ix.commitment(), outer);
let mut other = AxonIndex::new();
other.add(p(1), p(11));
other.add(p(1), p(10));
assert_eq!(ix.commitment(), other.commitment());
assert!(ix.remove_axon(&p(1), &p(11)));
assert!(!ix.remove_axon(&p(1), &p(11)));
assert_eq!(ix.max_degree(), 1);
assert!(ix.remove_axon(&p(1), &p(10)));
assert!(ix.get(&p(1)).is_none());
assert_eq!(ix.max_degree(), 0);
assert_eq!(ix.commitment(), EMPTY);
ix.insert(p(1), vec![p(11), p(10)]);
assert_eq!(ix.commitment(), other.commitment());
assert_eq!(ix.max_degree(), 2);
assert_eq!(ix.remove(&p(1)).map(|l| l.len()), Some(2));
assert_eq!(ix.commitment(), EMPTY);
}
#[test]
fn axon_index_matches_a_fresh_build_under_random_operations() {
let mut x: u64 = 0xA11CE;
let mut next = move || {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
};
let mut ix = AxonIndex::new();
let mut model: BTreeMap<Particle, Vec<Particle>> = BTreeMap::new();
for step in 0..2000u32 {
let particle = p((next() % 6) as u8);
let mut axon = [0u8; 32];
axon[..8].copy_from_slice(&(next() % 40).to_le_bytes());
match next() % 10 {
0..=5 => {
let list = model.entry(particle).or_default();
let new = !list.contains(&axon);
if new {
list.push(axon);
}
assert_eq!(ix.add(particle, axon), new);
}
6 => {
let had = model.get(&particle).is_some_and(|l| l.contains(&axon));
if let Some(l) = model.get_mut(&particle) {
l.retain(|a| a != &axon);
}
assert_eq!(ix.remove_axon(&particle, &axon), had);
}
7 => {
let len = (next() % 8) as usize;
let before = model.get(&particle).map_or(0, Vec::len);
if let Some(l) = model.get_mut(&particle) {
l.truncate(len);
}
assert_eq!(ix.truncate(&particle, len), before.saturating_sub(len));
}
8 => {
assert_eq!(ix.remove(&particle), model.remove(&particle).filter(|l| !l.is_empty()));
}
_ => {
let list: Vec<Particle> = (0..(next() % 5)).map(|i| p(100 + (i % 3) as u8)).collect();
let mut dedup: Vec<Particle> = Vec::new();
for a in &list {
if !dedup.contains(a) {
dedup.push(*a);
}
}
ix.insert(particle, list);
model.insert(particle, dedup);
}
}
model.retain(|_, l| !l.is_empty());
let mut fresh = AxonIndex::new();
for (k, list) in &model {
for a in list {
fresh.add(*k, *a);
}
}
assert_eq!(ix.commitment(), fresh.commitment(), "step {step}");
assert_eq!(&*ix, &model, "step {step}");
assert_eq!(ix.max_degree(), model.values().map(Vec::len).max().unwrap_or(0), "step {step}");
}
}
#[test]
fn axon_membership_proofs_verify() {
let mut ix = AxonIndex::new();
for i in 0..20u8 {
ix.add(p(1), p(100 + i));
}
let set_root = ix.set_root(&p(1)).unwrap();
let proof = ix.prove_member(&p(1), &p(105)).unwrap();
assert!(proof.verify(&set_root, &p(105), Some(&AxonIndex::member_digest(&p(105)))));
let absent = ix.prove_member(&p(1), &p(200)).unwrap();
assert!(absent.verify(&set_root, &p(200), None));
let outer = ix.prove(&p(1));
assert!(outer.verify(&ix.commitment(), &p(1), Some(&AxonIndex::leaf(&p(1), 20, &set_root))));
}
}