use crate::identity::AgentId;
use crate::kv::{KvEntry, KvError, KvStoreDelta, Result};
use saorsa_gossip_crdt_sync::{LwwRegister, OrSet};
use saorsa_gossip_types::PeerId;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AccessPolicy {
Signed,
Allowlisted,
Encrypted {
group_id: Vec<u8>,
},
}
impl std::fmt::Display for AccessPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Signed => write!(f, "signed"),
Self::Allowlisted => write!(f, "allowlisted"),
Self::Encrypted { .. } => write!(f, "encrypted"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct KvStoreId([u8; 32]);
impl KvStoreId {
#[must_use]
pub fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn from_content(name: &str, creator: &AgentId) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(b"x0x.store");
hasher.update(name.as_bytes());
hasher.update(creator.as_bytes());
Self(*hasher.finalize().as_bytes())
}
#[must_use]
pub fn for_topic_owner(topic: &str, owner: &AgentId) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(b"x0x.store.v2");
hasher.update(topic.as_bytes());
hasher.update(owner.as_bytes());
Self(*hasher.finalize().as_bytes())
}
}
impl std::fmt::Display for KvStoreId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
fn default_seq_counter() -> Arc<AtomicU64> {
Arc::new(AtomicU64::new(0))
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum AnchorChannel {
Creator,
RestParam,
#[default]
Persistence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum OwnershipSource {
Anchored {
owner: AgentId,
channel: AnchorChannel,
},
Unknown,
Conflict {
anchored: AgentId,
announced: AgentId,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OwnershipStatus {
Anchored,
Unknown,
Conflict,
}
const CHECKPOINT_SIG_DOMAIN: &[u8] = b"x0x.store.checkpoint.sig.v1";
const CHECKPOINT_ROOT_DOMAIN: &[u8] = b"x0x.store.checkpoint.root.v2";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnerCheckpoint {
pub topic: String,
pub store_id: KvStoreId,
pub owner_pubkey: Vec<u8>,
pub policy: AccessPolicy,
pub policy_version: u64,
pub checkpoint_seq: u64,
pub content_root: [u8; 32],
pub timestamp: u64,
pub signature: Vec<u8>,
}
impl OwnerCheckpoint {
#[must_use]
pub fn signing_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(256);
buf.extend_from_slice(CHECKPOINT_SIG_DOMAIN);
buf.extend_from_slice(self.topic.as_bytes());
buf.extend_from_slice(self.store_id.as_bytes());
buf.extend_from_slice(&self.owner_pubkey);
buf.extend_from_slice(&bincode::serialize(&self.policy).unwrap_or_default());
buf.extend_from_slice(&self.policy_version.to_le_bytes());
buf.extend_from_slice(&self.checkpoint_seq.to_le_bytes());
buf.extend_from_slice(&self.content_root);
buf.extend_from_slice(&self.timestamp.to_le_bytes());
buf
}
pub fn verify(&self, expected_owner: &AgentId) -> Result<()> {
use ant_quic::crypto::raw_public_keys::pqc::{verify_with_ml_dsa, MlDsaSignature};
let pubkey = ant_quic::MlDsaPublicKey::from_bytes(&self.owner_pubkey)
.map_err(|e| KvError::OwnerTokenInvalid(format!("bad owner pubkey: {e:?}")))?;
let derived = AgentId::from_public_key(&pubkey);
if &derived != expected_owner {
return Err(KvError::OwnerTokenInvalid(
"checkpoint owner_pubkey does not derive to the anchored owner".to_string(),
));
}
let sig = MlDsaSignature::from_bytes(&self.signature)
.map_err(|e| KvError::OwnerTokenInvalid(format!("bad signature: {e:?}")))?;
verify_with_ml_dsa(&pubkey, &self.signing_bytes(), &sig).map_err(|e| {
KvError::OwnerTokenInvalid(format!("invalid checkpoint signature: {e:?}"))
})?;
Ok(())
}
}
#[must_use]
pub fn content_root(store_id: &KvStoreId, name: &str, entries: &[(&str, &KvEntry)]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(CHECKPOINT_ROOT_DOMAIN);
h.update(store_id.as_bytes());
h.update(&(name.len() as u64).to_le_bytes());
h.update(name.as_bytes());
let mut sorted = entries.to_vec();
sorted.sort_by(|a, b| a.0.cmp(b.0));
for (outer_key, entry) in &sorted {
h.update(&entry_commitment_bytes(outer_key, entry));
}
*h.finalize().as_bytes()
}
fn entry_commitment_bytes(outer_key: &str, entry: &KvEntry) -> Vec<u8> {
let mut buf = Vec::with_capacity(256);
lp_bytes(&mut buf, outer_key.as_bytes());
lp_bytes(&mut buf, entry.key.as_bytes());
lp_bytes(&mut buf, &entry.value);
lp_bytes(&mut buf, &entry.content_hash);
lp_bytes(&mut buf, entry.content_type.as_bytes());
let mut meta: Vec<_> = entry.metadata.iter().collect();
meta.sort_by(|a, b| a.0.cmp(b.0));
buf.extend_from_slice(&(meta.len() as u64).to_le_bytes());
for (mk, mv) in &meta {
lp_bytes(&mut buf, mk.as_bytes());
lp_bytes(&mut buf, mv.as_bytes());
}
buf.extend_from_slice(&entry.created_at.to_le_bytes());
buf.extend_from_slice(&entry.updated_at.to_le_bytes());
buf
}
fn lp_bytes(buf: &mut Vec<u8>, data: &[u8]) {
buf.extend_from_slice(&(data.len() as u64).to_le_bytes());
buf.extend_from_slice(data);
}
fn validate_entry_integrity(outer_key: &str, entry: &KvEntry) -> Result<()> {
if outer_key != entry.key {
return Err(KvError::Merge(format!(
"checkpoint entry outer key {outer_key:?} != inner key {:?}",
entry.key
)));
}
let computed = *blake3::hash(&entry.value).as_bytes();
if computed != entry.content_hash {
return Err(KvError::Merge(
"checkpoint entry content_hash != blake3(value)".to_string(),
));
}
Ok(())
}
pub struct OwnerCheckpointParams<'a> {
pub topic: &'a str,
pub store_id: &'a KvStoreId,
pub secret_key: &'a ant_quic::MlDsaSecretKey,
pub public_key: &'a ant_quic::MlDsaPublicKey,
pub policy: &'a AccessPolicy,
pub policy_version: u64,
pub checkpoint_seq: u64,
pub content_root: [u8; 32],
pub timestamp: u64,
}
pub fn make_owner_checkpoint(params: OwnerCheckpointParams<'_>) -> Result<OwnerCheckpoint> {
use ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa;
let owner_pubkey = params.public_key.as_bytes().to_vec();
let cp = OwnerCheckpoint {
topic: params.topic.to_string(),
store_id: *params.store_id,
owner_pubkey,
policy: params.policy.clone(),
policy_version: params.policy_version,
checkpoint_seq: params.checkpoint_seq,
content_root: params.content_root,
timestamp: params.timestamp,
signature: Vec::new(),
};
let bytes = cp.signing_bytes();
let sig = sign_with_ml_dsa(params.secret_key, &bytes)
.map_err(|e| KvError::Gossip(format!("owner checkpoint sign failed: {e:?}")))?;
let mut cp = cp;
cp.signature = sig.as_bytes().to_vec();
Ok(cp)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KvStore {
id: KvStoreId,
keys: OrSet<String>,
entries: HashMap<String, KvEntry>,
name: LwwRegister<String>,
#[serde(default = "default_policy")]
policy: AccessPolicy,
#[serde(default)]
owner: Option<AgentId>,
#[serde(default)]
allowed_writers: HashSet<AgentId>,
#[serde(default)]
version: u64,
#[serde(default, deserialize_with = "de_tolerant")]
pub(crate) latest_checkpoint: Option<OwnerCheckpoint>,
#[serde(default, deserialize_with = "de_tolerant")]
pub(crate) highest_checkpoint_seq: u64,
#[serde(default, deserialize_with = "de_tolerant")]
anchor_channel: AnchorChannel,
#[serde(default, deserialize_with = "de_tolerant")]
policy_version: u64,
#[serde(default, deserialize_with = "de_tolerant")]
ownership_conflict: Option<(AgentId, AgentId)>,
#[serde(skip, default = "default_seq_counter")]
seq_counter: Arc<AtomicU64>,
}
fn de_tolerant<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de> + Default,
{
Ok(T::deserialize(deserializer).unwrap_or_default())
}
fn default_policy() -> AccessPolicy {
AccessPolicy::Signed
}
impl KvStore {
#[must_use]
pub fn new(id: KvStoreId, name: String, owner: AgentId, policy: AccessPolicy) -> Self {
let mut name_reg = LwwRegister::new(name.clone());
name_reg.set(name, PeerId::new(owner.0));
Self {
id,
keys: OrSet::new(),
entries: HashMap::new(),
name: name_reg,
policy,
owner: Some(owner),
anchor_channel: AnchorChannel::Creator,
policy_version: 0,
ownership_conflict: None,
latest_checkpoint: None,
highest_checkpoint_seq: 0,
allowed_writers: HashSet::new(),
version: 0,
seq_counter: Arc::new(AtomicU64::new(0)),
}
}
#[must_use]
pub fn new_replica(
id: KvStoreId,
name: String,
expected_owner: Option<AgentId>,
channel: AnchorChannel,
) -> Self {
Self {
id,
keys: OrSet::new(),
entries: HashMap::new(),
name: LwwRegister::new(name),
policy: AccessPolicy::Signed,
owner: expected_owner,
anchor_channel: channel,
policy_version: 0,
ownership_conflict: None,
latest_checkpoint: None,
highest_checkpoint_seq: 0,
allowed_writers: HashSet::new(),
version: 0,
seq_counter: Arc::new(AtomicU64::new(0)),
}
}
pub fn next_seq(&self) -> u64 {
self.seq_counter.fetch_add(1, Ordering::Relaxed) + 1
}
#[must_use]
pub fn current_version(&self) -> u64 {
self.version
}
#[must_use]
pub fn id(&self) -> &KvStoreId {
&self.id
}
#[must_use]
pub fn name(&self) -> &str {
self.name.get()
}
#[must_use]
pub fn name_register(&self) -> &LwwRegister<String> {
&self.name
}
#[must_use]
pub fn policy(&self) -> &AccessPolicy {
&self.policy
}
#[must_use]
pub fn owner(&self) -> Option<&AgentId> {
self.owner.as_ref()
}
#[must_use]
pub fn policy_version(&self) -> u64 {
self.policy_version
}
#[must_use]
pub fn anchor_channel(&self) -> AnchorChannel {
self.anchor_channel
}
#[must_use]
pub fn ownership_source(&self) -> OwnershipSource {
match (&self.owner, &self.ownership_conflict) {
(None, _) => OwnershipSource::Unknown,
(Some(owner), None) => OwnershipSource::Anchored {
owner: *owner,
channel: self.anchor_channel,
},
(Some(owner), Some((_anchored, announced))) => OwnershipSource::Conflict {
anchored: *owner,
announced: *announced,
},
}
}
#[must_use]
pub fn allowed_writers(&self) -> &HashSet<AgentId> {
&self.allowed_writers
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn is_authorized(&self, agent_id: &AgentId) -> bool {
match &self.policy {
AccessPolicy::Signed => {
self.owner.as_ref().is_some_and(|o| o == agent_id)
}
AccessPolicy::Allowlisted => {
self.owner.as_ref().is_some_and(|o| o == agent_id)
|| self.allowed_writers.contains(agent_id)
}
AccessPolicy::Encrypted { .. } => {
true
}
}
}
pub fn authorize_local_write(&self, writer: &AgentId) -> Result<()> {
if matches!(self.policy, AccessPolicy::Encrypted { .. }) {
return Ok(());
}
let Some(owner) = self.owner.as_ref() else {
return Err(KvError::OwnerUnknown);
};
if !self.is_authorized(writer) {
return Err(KvError::Unauthorized(format!(
"store policy is {}; owner is {}",
self.policy,
hex::encode(owner.as_bytes())
)));
}
Ok(())
}
pub fn learn_ownership(
&mut self,
claimed_owner: AgentId,
policy: AccessPolicy,
policy_version: u64,
verified_sender: &AgentId,
) -> Result<()> {
if *verified_sender != claimed_owner {
return Err(KvError::OwnerTokenInvalid(
"ownership announcement sender does not match claimed owner".to_string(),
));
}
let Some(existing) = self.owner else {
return Err(KvError::OwnerTokenInvalid(
"ownership cannot be learned from an announcement; supply expected_owner at join"
.to_string(),
));
};
if existing != claimed_owner {
self.ownership_conflict = Some((existing, claimed_owner));
self.version += 1;
return Err(KvError::OwnershipConflict {
anchored: existing,
claimed: claimed_owner,
});
}
if policy_version >= self.policy_version {
self.policy = policy;
self.policy_version = policy_version;
self.ownership_conflict = None;
self.version += 1;
}
Ok(())
}
pub fn allow_writer(&mut self, writer: AgentId, caller: &AgentId) -> Result<()> {
if !self.owner.as_ref().is_some_and(|o| o == caller) {
return Err(KvError::Unauthorized(
"only the store owner can modify the allowlist".to_string(),
));
}
self.allowed_writers.insert(writer);
self.policy_version = self.policy_version.saturating_add(1);
self.version += 1;
Ok(())
}
pub fn deny_writer(&mut self, writer: &AgentId, caller: &AgentId) -> Result<()> {
if !self.owner.as_ref().is_some_and(|o| o == caller) {
return Err(KvError::Unauthorized(
"only the store owner can modify the allowlist".to_string(),
));
}
self.allowed_writers.remove(writer);
self.policy_version = self.policy_version.saturating_add(1);
self.version += 1;
Ok(())
}
pub fn put(
&mut self,
key: String,
value: Vec<u8>,
content_type: String,
peer_id: PeerId,
) -> Result<()> {
if value.len() > crate::kv::entry::MAX_INLINE_SIZE {
return Err(KvError::ValueTooLarge {
size: value.len(),
max: crate::kv::entry::MAX_INLINE_SIZE,
});
}
let seq = self.next_seq();
self.keys
.add(key.clone(), (peer_id, seq))
.map_err(|e| KvError::Merge(format!("OR-Set add failed: {e}")))?;
if let Some(existing) = self.entries.get_mut(&key) {
existing.update_value(value, content_type);
} else {
self.entries
.insert(key.clone(), KvEntry::new(key, value, content_type));
}
self.version += 1;
Ok(())
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&KvEntry> {
if self.keys.contains(&key.to_string()) {
self.entries.get(key)
} else {
None
}
}
pub fn remove(&mut self, key: &str) -> Result<()> {
if !self.entries.contains_key(key) {
return Err(KvError::KeyNotFound(key.to_string()));
}
self.keys
.remove(&key.to_string())
.map_err(|e| KvError::Merge(format!("OR-Set remove failed: {e}")))?;
self.entries.remove(key);
self.version += 1;
Ok(())
}
#[must_use]
pub fn active_keys(&self) -> Vec<&String> {
self.keys.elements().into_iter().collect()
}
#[must_use]
pub fn active_entries(&self) -> Vec<&KvEntry> {
let active: HashSet<String> = self.keys.elements().into_iter().cloned().collect();
self.entries
.values()
.filter(|e| active.contains(&e.key))
.collect()
}
pub(crate) fn checkpoint_pairs(&self) -> Vec<(&str, &KvEntry)> {
self.keys
.elements()
.into_iter()
.filter_map(|k| self.entries.get(k).map(|e| (k.as_str(), e)))
.collect()
}
pub fn update_name(&mut self, name: String, peer_id: PeerId) {
self.name.set(name, peer_id);
self.version += 1;
}
pub fn merge_delta(
&mut self,
delta: &KvStoreDelta,
peer_id: PeerId,
writer: Option<&AgentId>,
) -> Result<()> {
if let Some(cp) = delta.owner_checkpoint.as_ref() {
if self.try_adopt_full_snapshot(delta, peer_id, cp) {
return Ok(());
}
}
if let Some(writer_id) = writer {
if !self.is_authorized(writer_id) {
tracing::warn!(
"rejected delta from unauthorized writer {} for store {}",
hex::encode(writer_id.as_bytes()),
self.id
);
return Ok(()); }
} else {
match &self.policy {
AccessPolicy::Encrypted { .. } => {} _ => {
tracing::warn!(
"rejected anonymous delta for non-encrypted store {}",
self.id
);
return Ok(());
}
}
}
if let Some(ref additions) = delta.allowlist_additions {
if writer.is_some_and(|w| self.owner.as_ref().is_some_and(|o| o == w)) {
for agent in additions {
self.allowed_writers.insert(*agent);
}
}
}
if let Some(ref removals) = delta.allowlist_removals {
if writer.is_some_and(|w| self.owner.as_ref().is_some_and(|o| o == w)) {
for agent in removals {
self.allowed_writers.remove(agent);
}
}
}
for (key, (entry, tag)) in &delta.added {
self.keys
.add(key.clone(), *tag)
.map_err(|e| KvError::Merge(format!("OR-Set add failed: {e}")))?;
if let Some(existing) = self.entries.get_mut(key) {
existing.merge(entry);
} else {
self.entries.insert(key.clone(), entry.clone());
}
}
for key in delta.removed.keys() {
let _ = self.keys.remove(&key.to_string());
self.entries.remove(key.as_str());
}
for (key, entry) in &delta.updated {
if let Some(existing) = self.entries.get_mut(key) {
existing.merge(entry);
} else {
self.keys
.add(key.clone(), (peer_id, 0))
.map_err(|e| KvError::Merge(format!("OR-Set add failed: {e}")))?;
self.entries.insert(key.clone(), entry.clone());
}
}
if let Some(ref name_register) = delta.name_update {
self.name.merge(name_register);
}
if let Some(cp) = delta.owner_checkpoint.as_ref() {
self.maybe_cache_checkpoint(cp);
}
self.version += 1;
Ok(())
}
fn try_adopt_full_snapshot(
&mut self,
delta: &KvStoreDelta,
peer_id: PeerId,
cp: &OwnerCheckpoint,
) -> bool {
let Some(expected_owner) = self.owner else {
tracing::warn!("rejected owner checkpoint for unanchored store {}", self.id);
return false;
};
if let Err(e) = cp.verify(&expected_owner) {
tracing::warn!("rejected owner checkpoint for store {}: {e}", self.id);
return false;
}
if cp.store_id != self.id {
tracing::warn!(
"rejected owner checkpoint: store_id mismatch for {}",
self.id
);
return false;
}
if cp.checkpoint_seq <= self.highest_checkpoint_seq {
return false; }
for (key, (entry, _)) in &delta.added {
if let Err(e) = validate_entry_integrity(key, entry) {
tracing::warn!("rejected checkpoint entry for store {}: {e}", self.id);
return false;
}
}
for (key, entry) in &delta.updated {
if let Err(e) = validate_entry_integrity(key, entry) {
tracing::warn!("rejected checkpoint entry for store {}: {e}", self.id);
return false;
}
}
let mut relayed: Vec<(&str, &KvEntry)> = delta
.added
.iter()
.map(|(k, (e, _))| (k.as_str(), e))
.collect();
relayed.extend(delta.updated.iter().map(|(k, e)| (k.as_str(), e)));
let relayed_name: &str = delta
.name_update
.as_ref()
.map(|r| r.get().as_str())
.unwrap_or("");
if content_root(&self.id, relayed_name, &relayed) != cp.content_root {
return false; }
for (key, (entry, tag)) in &delta.added {
let _ = self.keys.add(key.clone(), *tag);
if let Some(existing) = self.entries.get_mut(key) {
existing.merge(entry);
} else {
self.entries.insert(key.clone(), entry.clone());
}
}
for (key, entry) in &delta.updated {
if let Some(existing) = self.entries.get_mut(key) {
existing.merge(entry);
} else {
let _ = self.keys.add(key.clone(), (peer_id, 0));
self.entries.insert(key.clone(), entry.clone());
}
}
let signed_keys: std::collections::HashSet<&str> = delta
.added
.keys()
.map(String::as_str)
.chain(delta.updated.keys().map(String::as_str))
.collect();
let stale: Vec<String> = self
.keys
.elements()
.into_iter()
.filter(|k| !signed_keys.contains(k.as_str()))
.cloned()
.collect();
for key in stale {
let _ = self.keys.remove(&key);
self.entries.remove(&key);
}
if let Some(name_register) = &delta.name_update {
self.name.merge(name_register);
}
if cp.policy_version >= self.policy_version {
self.policy = cp.policy.clone();
self.policy_version = cp.policy_version;
}
self.latest_checkpoint = Some(cp.clone());
self.highest_checkpoint_seq = cp.checkpoint_seq;
self.version += 1;
true
}
fn maybe_cache_checkpoint(&mut self, cp: &OwnerCheckpoint) {
if cp.checkpoint_seq <= self.highest_checkpoint_seq {
return;
}
let Some(expected_owner) = self.owner else {
return;
};
if cp.verify(&expected_owner).is_err() {
return;
}
if cp.store_id != self.id {
return;
}
let matches = {
let pairs = self.checkpoint_pairs();
content_root(&self.id, self.name(), &pairs) == cp.content_root
};
if matches {
self.latest_checkpoint = Some(cp.clone());
self.highest_checkpoint_seq = cp.checkpoint_seq;
}
}
pub fn merge(&mut self, other: &KvStore) -> Result<()> {
if self.id != other.id {
return Err(KvError::StoreIdMismatch);
}
self.keys
.merge_state(&other.keys)
.map_err(|e| KvError::Merge(format!("OR-Set merge failed: {e}")))?;
for (key, other_entry) in &other.entries {
if let Some(our_entry) = self.entries.get_mut(key) {
our_entry.merge(other_entry);
} else {
self.entries.insert(key.clone(), other_entry.clone());
}
}
for writer in &other.allowed_writers {
self.allowed_writers.insert(*writer);
}
self.name.merge(&other.name);
self.version += 1;
Ok(())
}
#[must_use]
pub fn full_delta(&self) -> KvStoreDelta {
let mut delta = KvStoreDelta::new(self.version);
for key in self.keys.elements() {
if let Some(entry) = self.entries.get(key) {
let tag = (PeerId::new([0u8; 32]), 0);
delta.added.insert(key.clone(), (entry.clone(), tag));
}
}
delta.name_update = Some(self.name.clone());
if !self.allowed_writers.is_empty() {
delta.allowlist_additions = Some(self.allowed_writers.iter().copied().collect());
}
delta.owner_checkpoint = self.latest_checkpoint.clone();
delta
}
}
#[cfg(test)]
mod tests {
use super::*;
fn agent(n: u8) -> AgentId {
AgentId([n; 32])
}
fn peer(n: u8) -> PeerId {
PeerId::new([n; 32])
}
fn store_id(n: u8) -> KvStoreId {
KvStoreId::new([n; 32])
}
#[test]
fn test_new_store() {
let owner = agent(1);
let store = KvStore::new(store_id(1), "Test".to_string(), owner, AccessPolicy::Signed);
assert_eq!(store.name(), "Test");
assert_eq!(store.len(), 0);
assert!(store.is_empty());
assert_eq!(store.owner(), Some(&owner));
assert_eq!(*store.policy(), AccessPolicy::Signed);
}
#[test]
fn test_put_and_get() {
let p = peer(1);
let mut store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
store
.put(
"key1".to_string(),
b"hello".to_vec(),
"text/plain".to_string(),
p,
)
.expect("put");
let entry = store.get("key1").expect("get");
assert_eq!(entry.value, b"hello");
assert_eq!(store.len(), 1);
}
#[test]
fn test_put_update() {
let p = peer(1);
let mut store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
store
.put(
"key1".to_string(),
b"old".to_vec(),
"text/plain".to_string(),
p,
)
.expect("put");
store
.put(
"key1".to_string(),
b"new".to_vec(),
"text/plain".to_string(),
p,
)
.expect("put");
assert_eq!(store.get("key1").expect("get").value, b"new");
assert_eq!(store.len(), 1);
}
#[test]
fn test_remove() {
let p = peer(1);
let mut store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
store
.put(
"key1".to_string(),
b"val".to_vec(),
"text/plain".to_string(),
p,
)
.expect("put");
store.remove("key1").expect("remove");
assert!(store.get("key1").is_none());
}
#[test]
fn test_remove_nonexistent() {
let mut store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
assert!(store.remove("nope").is_err());
}
#[test]
fn test_value_too_large() {
let p = peer(1);
let mut store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
let big = vec![0u8; 100_000];
let result = store.put(
"big".to_string(),
big,
"application/octet-stream".to_string(),
p,
);
assert!(result.is_err());
}
#[test]
fn test_active_keys() {
let p = peer(1);
let mut store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
store
.put("a".to_string(), b"1".to_vec(), "text/plain".to_string(), p)
.expect("put");
store
.put("b".to_string(), b"2".to_vec(), "text/plain".to_string(), p)
.expect("put");
store
.put("c".to_string(), b"3".to_vec(), "text/plain".to_string(), p)
.expect("put");
assert_eq!(store.active_keys().len(), 3);
}
#[test]
fn test_merge_stores() {
let p1 = peer(1);
let p2 = peer(2);
let id = store_id(1);
let owner = agent(1);
let mut s1 = KvStore::new(id, "Store".to_string(), owner, AccessPolicy::Signed);
let mut s2 = KvStore::new(id, "Store".to_string(), owner, AccessPolicy::Signed);
s1.put("a".to_string(), b"1".to_vec(), "text/plain".to_string(), p1)
.expect("put");
s2.put("b".to_string(), b"2".to_vec(), "text/plain".to_string(), p2)
.expect("put");
s1.merge(&s2).expect("merge");
assert_eq!(s1.len(), 2);
}
#[test]
fn test_merge_different_ids_fails() {
let owner = agent(1);
let mut s1 = KvStore::new(store_id(1), "A".to_string(), owner, AccessPolicy::Signed);
let s2 = KvStore::new(store_id(2), "B".to_string(), owner, AccessPolicy::Signed);
assert!(s1.merge(&s2).is_err());
}
#[test]
fn test_version_increments() {
let p = peer(1);
let mut store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
assert_eq!(store.current_version(), 0);
store
.put("k".to_string(), b"v".to_vec(), "text/plain".to_string(), p)
.expect("put");
assert_eq!(store.current_version(), 1);
store.remove("k").expect("remove");
assert_eq!(store.current_version(), 2);
}
#[test]
fn test_store_id_from_content() {
let a = agent(1);
let id1 = KvStoreId::from_content("store1", &a);
let id2 = KvStoreId::from_content("store1", &a);
let id3 = KvStoreId::from_content("store2", &a);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_serialization_roundtrip() {
let p = peer(1);
let mut store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
store
.put(
"key1".to_string(),
b"val".to_vec(),
"text/plain".to_string(),
p,
)
.expect("put");
let bytes = bincode::serialize(&store).expect("serialize");
let restored: KvStore = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(store.id(), restored.id());
assert_eq!(store.name(), restored.name());
assert_eq!(store.len(), restored.len());
}
#[test]
fn pre_wave_kvstore_blob_decodes_with_trailing_fields_defaulted() {
#[derive(Serialize)]
struct PreWaveKvStore<'a> {
id: &'a KvStoreId,
keys: &'a OrSet<String>,
entries: &'a HashMap<String, KvEntry>,
name: &'a LwwRegister<String>,
policy: &'a AccessPolicy,
owner: &'a Option<AgentId>,
allowed_writers: &'a HashSet<AgentId>,
version: u64,
}
let owner = agent(1);
let mut store = KvStore::new(
store_id(1),
"Legacy".to_string(),
owner,
AccessPolicy::Signed,
);
store
.put(
"k".to_string(),
b"v".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("put");
let legacy = PreWaveKvStore {
id: &store.id,
keys: &store.keys,
entries: &store.entries,
name: &store.name,
policy: &store.policy,
owner: &store.owner,
allowed_writers: &store.allowed_writers,
version: store.version,
};
let bytes = bincode::serialize(&legacy).expect("serialize pre-wave shape");
let restored: KvStore =
bincode::deserialize(&bytes).expect("pre-wave blob (id..version) must decode");
assert_eq!(restored.id(), store.id(), "id preserved");
assert_eq!(restored.len(), 1, "entries preserved");
assert_eq!(restored.owner, store.owner, "owner preserved");
assert!(
restored.latest_checkpoint.is_none(),
"latest_checkpoint defaults"
);
assert_eq!(restored.highest_checkpoint_seq, 0, "high-water defaults");
assert_eq!(
restored.anchor_channel,
AnchorChannel::default(),
"anchor_channel defaults"
);
assert_eq!(restored.policy_version, 0, "policy_version defaults");
assert!(
restored.ownership_conflict.is_none(),
"ownership_conflict defaults"
);
}
#[test]
fn test_next_seq_monotonic() {
let store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Signed,
);
let s1 = store.next_seq();
let s2 = store.next_seq();
assert!(s2 > s1);
}
#[test]
fn test_signed_policy_owner_authorized() {
let owner = agent(1);
let store = KvStore::new(store_id(1), "Test".to_string(), owner, AccessPolicy::Signed);
assert!(store.is_authorized(&owner));
}
#[test]
fn test_signed_policy_non_owner_rejected() {
let owner = agent(1);
let other = agent(2);
let store = KvStore::new(store_id(1), "Test".to_string(), owner, AccessPolicy::Signed);
assert!(!store.is_authorized(&other));
}
#[test]
fn test_signed_policy_rejects_unauthorized_delta() {
let owner = agent(1);
let attacker = agent(99);
let mut store = KvStore::new(store_id(1), "Test".to_string(), owner, AccessPolicy::Signed);
let entry = KvEntry::new(
"spam".to_string(),
b"junk".to_vec(),
"text/plain".to_string(),
);
let delta = KvStoreDelta::for_put("spam".to_string(), entry, (peer(99), 1), 1);
store
.merge_delta(&delta, peer(99), Some(&attacker))
.expect("should not error");
assert!(store.get("spam").is_none(), "spam should be rejected");
}
#[test]
fn test_signed_policy_accepts_owner_delta() {
let owner = agent(1);
let mut store = KvStore::new(store_id(1), "Test".to_string(), owner, AccessPolicy::Signed);
let entry = KvEntry::new(
"legit".to_string(),
b"data".to_vec(),
"text/plain".to_string(),
);
let delta = KvStoreDelta::for_put("legit".to_string(), entry, (peer(1), 1), 1);
store
.merge_delta(&delta, peer(1), Some(&owner))
.expect("merge");
assert!(store.get("legit").is_some());
}
#[test]
fn test_allowlisted_policy() {
let owner = agent(1);
let writer = agent(2);
let outsider = agent(3);
let mut store = KvStore::new(
store_id(1),
"Team".to_string(),
owner,
AccessPolicy::Allowlisted,
);
store.allow_writer(writer, &owner).expect("allow");
assert!(store.is_authorized(&owner));
assert!(store.is_authorized(&writer));
assert!(!store.is_authorized(&outsider));
}
#[test]
fn test_allowlisted_rejects_non_owner_allowlist_change() {
let owner = agent(1);
let other = agent(2);
let mut store = KvStore::new(
store_id(1),
"Team".to_string(),
owner,
AccessPolicy::Allowlisted,
);
let result = store.allow_writer(agent(3), &other);
assert!(result.is_err());
}
#[test]
fn test_deny_writer() {
let owner = agent(1);
let writer = agent(2);
let mut store = KvStore::new(
store_id(1),
"Team".to_string(),
owner,
AccessPolicy::Allowlisted,
);
store.allow_writer(writer, &owner).expect("allow");
assert!(store.is_authorized(&writer));
store.deny_writer(&writer, &owner).expect("deny");
assert!(!store.is_authorized(&writer));
}
#[test]
fn test_allowlist_delta_propagation() {
let owner = agent(1);
let writer = agent(2);
let mut store = KvStore::new(
store_id(1),
"Team".to_string(),
owner,
AccessPolicy::Allowlisted,
);
store.allow_writer(writer, &owner).expect("allow");
let delta = store.full_delta();
assert!(delta.allowlist_additions.is_some());
assert!(delta
.allowlist_additions
.as_ref()
.is_some_and(|a| a.contains(&writer)));
}
#[test]
fn test_anonymous_delta_rejected_for_signed_store() {
let owner = agent(1);
let mut store = KvStore::new(store_id(1), "Test".to_string(), owner, AccessPolicy::Signed);
let entry = KvEntry::new(
"anon".to_string(),
b"spam".to_vec(),
"text/plain".to_string(),
);
let delta = KvStoreDelta::for_put("anon".to_string(), entry, (peer(99), 1), 1);
store
.merge_delta(&delta, peer(99), None)
.expect("silent rejection");
assert!(store.get("anon").is_none());
}
#[test]
fn test_local_write_owner_authorized_on_signed_store() {
let owner = agent(1);
let store = KvStore::new(store_id(1), "Test".to_string(), owner, AccessPolicy::Signed);
store
.authorize_local_write(&owner)
.expect("owner must be able to write locally");
}
#[test]
fn test_local_write_non_owner_rejected_on_signed_store() {
let owner = agent(1);
let joiner = agent(2);
let store = KvStore::new(store_id(1), "Test".to_string(), owner, AccessPolicy::Signed);
let err = store
.authorize_local_write(&joiner)
.expect_err("non-owner local write must be rejected, not silently applied");
assert!(matches!(err, KvError::Unauthorized(_)));
assert!(
format!("{err}").contains(&hex::encode(owner.as_bytes())),
"rejection must name the true owner so the caller can tell why"
);
}
#[test]
fn test_no_anchor_join_is_read_only_unknown() {
let joiner = agent(2);
let store =
KvStore::new_replica(store_id(1), String::new(), None, AnchorChannel::Persistence);
assert!(
store.owner().is_none(),
"no-anchor replica must not claim an owner"
);
assert!(matches!(store.ownership_source(), OwnershipSource::Unknown));
let err = store
.authorize_local_write(&joiner)
.expect_err("write on no-anchor store must fail closed");
assert!(matches!(err, KvError::OwnerUnknown));
}
#[test]
fn test_no_anchor_replica_rejects_inbound_deltas() {
let mut store =
KvStore::new_replica(store_id(1), String::new(), None, AnchorChannel::Persistence);
let entry = KvEntry::new("k".to_string(), b"v".to_vec(), "text/plain".to_string());
let delta = KvStoreDelta::for_put("k".to_string(), entry, (peer(9), 1), 1);
store
.merge_delta(&delta, peer(9), Some(&agent(9)))
.expect("silent rejection");
assert!(store.get("k").is_none());
}
#[test]
fn test_learn_ownership_rejects_third_party_assignment() {
let mut store =
KvStore::new_replica(store_id(1), String::new(), None, AnchorChannel::Persistence);
let owner = agent(1);
let rogue = agent(9);
let err = store
.learn_ownership(owner, AccessPolicy::Signed, 0, &rogue)
.expect_err("third-party ownership claim must be rejected");
assert!(matches!(err, KvError::OwnerTokenInvalid(_)));
assert!(store.owner().is_none());
}
#[test]
fn test_learn_ownership_rejects_none_to_some_first_capture_guard() {
let mut store =
KvStore::new_replica(store_id(1), String::new(), None, AnchorChannel::Persistence);
let rogue = agent(9);
let err = store
.learn_ownership(rogue, AccessPolicy::Signed, 0, &rogue)
.expect_err("self-claim must not establish ownership on a no-anchor store");
assert!(matches!(err, KvError::OwnerTokenInvalid(_)));
assert!(
store.owner().is_none(),
"ownership must remain unestablished"
);
assert!(matches!(store.ownership_source(), OwnershipSource::Unknown));
}
#[test]
fn test_anchored_join_merges_owner_deltas_rejects_rogue() {
let owner = agent(1);
let joiner = agent(2);
let rogue = agent(9);
let mut store = KvStore::new_replica(
store_id(1),
String::new(),
Some(owner),
AnchorChannel::RestParam,
);
assert_eq!(store.owner(), Some(&owner));
assert!(matches!(
store.ownership_source(),
OwnershipSource::Anchored {
owner: _,
channel: AnchorChannel::RestParam
}
));
assert!(matches!(
store.authorize_local_write(&joiner),
Err(KvError::Unauthorized(_))
));
let entry = KvEntry::new("ok".to_string(), b"v".to_vec(), "text/plain".to_string());
let delta = KvStoreDelta::for_put("ok".to_string(), entry, (peer(1), 1), 1);
store
.merge_delta(&delta, peer(1), Some(&owner))
.expect("owner delta merges");
assert!(store.get("ok").is_some());
let entry = KvEntry::new("bad".to_string(), b"x".to_vec(), "text/plain".to_string());
let delta = KvStoreDelta::for_put("bad".to_string(), entry, (peer(9), 1), 2);
store
.merge_delta(&delta, peer(9), Some(&rogue))
.expect("silent rejection");
assert!(store.get("bad").is_none());
}
#[test]
fn test_first_capture_impossible_against_anchored_joiner() {
let owner = agent(1);
let rogue = agent(9);
let mut store = KvStore::new_replica(
store_id(1),
String::new(),
Some(owner),
AnchorChannel::RestParam,
);
let err = store
.learn_ownership(rogue, AccessPolicy::Allowlisted, 5, &rogue)
.expect_err("rogue self-claim against anchored owner must conflict");
assert!(
matches!(err, KvError::OwnershipConflict { anchored, claimed } if anchored == owner && claimed == rogue)
);
assert_eq!(store.owner(), Some(&owner), "anchored owner unchanged");
assert!(matches!(
store.ownership_source(),
OwnershipSource::Conflict { anchored, announced }
if anchored == owner && announced == rogue
));
store
.learn_ownership(owner, AccessPolicy::Allowlisted, 1, &owner)
.expect("owner policy refresh");
assert_eq!(*store.policy(), AccessPolicy::Allowlisted);
assert!(matches!(
store.ownership_source(),
OwnershipSource::Anchored {
owner: _,
channel: AnchorChannel::RestParam
}
));
}
#[test]
fn test_ownership_immutable_conflict() {
let owner = agent(1);
let hijacker = agent(9);
let mut store = KvStore::new_replica(
store_id(1),
String::new(),
Some(owner),
AnchorChannel::RestParam,
);
let err = store
.learn_ownership(hijacker, AccessPolicy::Signed, 0, &hijacker)
.expect_err("conflicting ownership claim must be rejected");
assert!(
matches!(err, KvError::OwnershipConflict { anchored, claimed } if anchored == owner && claimed == hijacker)
);
assert_eq!(store.owner(), Some(&owner));
assert!(matches!(
store.ownership_source(),
OwnershipSource::Conflict { anchored, announced }
if anchored == owner && announced == hijacker
));
}
#[test]
fn test_policy_refresh_monotonic_blocks_replay_downgrade() {
let owner = agent(1);
let mut store = KvStore::new_replica(
store_id(1),
String::new(),
Some(owner),
AnchorChannel::RestParam,
);
store
.learn_ownership(owner, AccessPolicy::Allowlisted, 2, &owner)
.expect("forward refresh applies");
assert_eq!(*store.policy(), AccessPolicy::Allowlisted);
assert_eq!(store.policy_version(), 2);
store
.learn_ownership(owner, AccessPolicy::Signed, 1, &owner)
.expect("stale replay dropped without error");
assert_eq!(
*store.policy(),
AccessPolicy::Allowlisted,
"policy not downgraded"
);
assert_eq!(store.policy_version(), 2);
}
#[test]
fn test_store_id_for_topic_owner_binds_owner() {
let owner = agent(1);
let rogue = agent(9);
let creator_id = KvStoreId::for_topic_owner("store/x", &owner);
let joiner_id = KvStoreId::for_topic_owner("store/x", &owner);
assert_eq!(creator_id, joiner_id, "creator and joiner agree on id");
assert_ne!(
KvStoreId::for_topic_owner("store/x", &owner),
KvStoreId::for_topic_owner("store/x", &rogue),
"different owner => different id"
);
assert_ne!(
KvStoreId::for_topic_owner("store/x", &owner),
KvStoreId::from_content("store/x", &owner)
);
}
#[test]
fn test_v0_30_1_owner_converges_with_anchored_joiner_no_announce() {
let owner = agent(1);
let joiner_agent = agent(2);
let mut owner_store =
KvStore::new(store_id(1), "S".to_string(), owner, AccessPolicy::Signed);
owner_store
.put(
"k".to_string(),
b"v".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("owner put");
let full = owner_store.full_delta();
let mut joiner = KvStore::new_replica(
store_id(1),
String::new(),
Some(owner),
AnchorChannel::RestParam,
);
joiner
.merge_delta(&full, peer(1), Some(&owner))
.expect("owner full-delta merges on an anchored joiner");
assert!(
joiner.get("k").is_some(),
"anchored joiner converges against a v0.30.1 owner with no announce"
);
assert!(matches!(
joiner.authorize_local_write(&joiner_agent),
Err(KvError::Unauthorized(_))
));
let mut unanchored =
KvStore::new_replica(store_id(1), String::new(), None, AnchorChannel::Persistence);
unanchored
.merge_delta(&full, peer(1), Some(&owner))
.expect("silent rejection");
assert!(unanchored.get("k").is_none());
}
#[test]
fn test_local_write_encrypted_policy_stays_permissive() {
let store = KvStore::new(
store_id(1),
"Test".to_string(),
agent(1),
AccessPolicy::Encrypted { group_id: vec![1] },
);
store
.authorize_local_write(&agent(9))
.expect("encrypted policy is permissive at the store layer");
}
#[test]
fn test_policy_display() {
assert_eq!(format!("{}", AccessPolicy::Signed), "signed");
assert_eq!(format!("{}", AccessPolicy::Allowlisted), "allowlisted");
assert_eq!(
format!(
"{}",
AccessPolicy::Encrypted {
group_id: vec![1, 2, 3]
}
),
"encrypted"
);
}
fn checkpoint_for(
store: &KvStore,
topic: &str,
kp: &crate::identity::AgentKeypair,
seq: u64,
) -> OwnerCheckpoint {
let pairs = store.checkpoint_pairs();
let root = content_root(store.id(), store.name(), &pairs);
make_owner_checkpoint(OwnerCheckpointParams {
topic,
store_id: store.id(),
secret_key: kp.secret_key(),
public_key: kp.public_key(),
policy: store.policy(),
policy_version: store.policy_version(),
checkpoint_seq: seq,
content_root: root,
timestamp: 0,
})
.expect("sign checkpoint")
}
#[test]
fn cold_join_from_replica_with_owner_offline() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let relayer = agent(9);
let topic = "store/cold";
let id = KvStoreId::for_topic_owner(topic, &owner);
let mut owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
owner_store
.put(
"k".to_string(),
b"v".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("put");
let cp = checkpoint_for(&owner_store, topic, &kp, 1);
let mut delta = owner_store.full_delta();
delta.owner_checkpoint = Some(cp.clone());
let mut joiner =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
joiner
.merge_delta(&delta, peer(9), Some(&relayer))
.expect("checkpoint-gated merge");
assert!(joiner.get("k").is_some(), "adopted relayed owner content");
assert_eq!(joiner.highest_checkpoint_seq, 1);
}
#[test]
fn full_replace_ignores_relay_injected_removed() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/inject";
let id = KvStoreId::for_topic_owner(topic, &owner);
let mut owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
owner_store
.put(
"k".to_string(),
b"v".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("put");
let cp = checkpoint_for(&owner_store, topic, &kp, 1);
let mut delta = owner_store.full_delta();
delta.owner_checkpoint = Some(cp);
let mut tags = std::collections::HashSet::new();
tags.insert((peer(9), 1));
delta.removed.insert("k".to_string(), tags);
let mut joiner =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
joiner
.merge_delta(&delta, peer(9), Some(&agent(9)))
.expect("merge");
assert!(
joiner.get("k").is_some(),
"injected removed must not truncate the owner-signed set"
);
assert_eq!(joiner.highest_checkpoint_seq, 1, "checkpoint adopted");
}
#[test]
fn full_replace_drops_keys_absent_from_newer_checkpoint() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/delrec";
let id = KvStoreId::for_topic_owner(topic, &owner);
let mut owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
owner_store
.put(
"k1".to_string(),
b"a".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("put k1");
owner_store
.put(
"k2".to_string(),
b"b".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("put k2");
let cp1 = checkpoint_for(&owner_store, topic, &kp, 1);
let mut d1 = owner_store.full_delta();
d1.owner_checkpoint = Some(cp1);
let mut joiner =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
joiner
.merge_delta(&d1, peer(9), Some(&agent(9)))
.expect("adopt cp1");
assert!(
joiner.get("k1").is_some() && joiner.get("k2").is_some(),
"joiner recovered both keys from cp1"
);
owner_store.remove("k2").expect("delete k2");
let cp2 = checkpoint_for(&owner_store, topic, &kp, 2);
let mut d2 = owner_store.full_delta();
d2.owner_checkpoint = Some(cp2);
joiner
.merge_delta(&d2, peer(9), Some(&agent(9)))
.expect("adopt cp2");
assert!(joiner.get("k1").is_some(), "k1 survives");
assert!(
joiner.get("k2").is_none(),
"k2 dropped by full-replace, not resurrected"
);
assert_eq!(joiner.highest_checkpoint_seq, 2);
}
#[test]
fn relay_tamper_rejected() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/tamper";
let id = KvStoreId::for_topic_owner(topic, &owner);
let mut owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
owner_store
.put(
"k".to_string(),
b"v".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("put");
let cp = checkpoint_for(&owner_store, topic, &kp, 1);
let mut delta = owner_store.full_delta();
if let Some((e, _)) = delta.added.get_mut("k") {
e.value = b"TAMPERED".to_vec();
e.content_hash = *blake3::hash(b"TAMPERED").as_bytes();
}
delta.owner_checkpoint = Some(cp);
let mut joiner =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
joiner
.merge_delta(&delta, peer(9), Some(&agent(9)))
.expect("no error");
assert!(
joiner.get("k").is_none(),
"tampered relay must not be adopted"
);
assert_eq!(joiner.highest_checkpoint_seq, 0);
}
#[test]
fn checkpoint_replay_downgrade_rejected() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/replay";
let id = KvStoreId::for_topic_owner(topic, &owner);
let owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
let cp_old = checkpoint_for(&owner_store, topic, &kp, 1);
let mut joiner =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
joiner.highest_checkpoint_seq = 5; let mut delta = KvStoreDelta::new(0);
delta.owner_checkpoint = Some(cp_old);
joiner
.merge_delta(&delta, peer(9), Some(&agent(9)))
.expect("no error");
assert_eq!(joiner.highest_checkpoint_seq, 5, "stale replay dropped");
}
#[test]
fn unanchored_joiner_rejects_checkpoint() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/unanchored";
let id = KvStoreId::for_topic_owner(topic, &owner);
let owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
let cp = checkpoint_for(&owner_store, topic, &kp, 1);
let mut delta = KvStoreDelta::new(0);
delta.owner_checkpoint = Some(cp);
let mut joiner = KvStore::new_replica(id, String::new(), None, AnchorChannel::Persistence);
joiner
.merge_delta(&delta, peer(9), Some(&agent(9)))
.expect("no error");
assert!(
joiner.owner().is_none(),
"owner never learned from checkpoint"
);
assert!(joiner.get("k").is_none());
}
#[test]
fn cross_store_checkpoint_replay_rejected() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic_a = "store/A";
let id_a = KvStoreId::for_topic_owner(topic_a, &owner);
let owner_store = KvStore::new(id_a, "A".to_string(), owner, AccessPolicy::Signed);
let cp = checkpoint_for(&owner_store, topic_a, &kp, 1);
let id_b = KvStoreId::for_topic_owner("store/B", &owner);
let mut delta = KvStoreDelta::new(0);
delta.owner_checkpoint = Some(cp);
let mut joiner_b =
KvStore::new_replica(id_b, String::new(), Some(owner), AnchorChannel::RestParam);
joiner_b
.merge_delta(&delta, peer(9), Some(&agent(9)))
.expect("no error");
assert_eq!(
joiner_b.highest_checkpoint_seq, 0,
"cross-store replay rejected"
);
}
#[test]
fn checkpoint_verify_rejects_forged_owner() {
let owner = agent(1);
let rogue_kp = crate::identity::AgentKeypair::generate().expect("rogue keypair");
let topic = "store/forged";
let id = KvStoreId::for_topic_owner(topic, &owner);
let cp = make_owner_checkpoint(OwnerCheckpointParams {
topic,
store_id: &id,
secret_key: rogue_kp.secret_key(),
public_key: rogue_kp.public_key(),
policy: &AccessPolicy::Signed,
policy_version: 0,
checkpoint_seq: 1,
content_root: [0u8; 32],
timestamp: 0,
})
.expect("sign");
let err = cp.verify(&owner).expect_err("forged checkpoint rejected");
assert!(matches!(err, KvError::OwnerTokenInvalid(_)));
}
#[test]
fn owner_returns_advances_high_water_mark() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/return";
let id = KvStoreId::for_topic_owner(topic, &owner);
let mut owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
owner_store
.put(
"k".to_string(),
b"v1".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("put");
let cp1 = checkpoint_for(&owner_store, topic, &kp, 1);
let mut relay = owner_store.full_delta();
relay.owner_checkpoint = Some(cp1);
let mut joiner =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
joiner
.merge_delta(&relay, peer(9), Some(&agent(9)))
.expect("relay adopt");
assert_eq!(joiner.highest_checkpoint_seq, 1);
owner_store
.put(
"k2".to_string(),
b"v2".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("put");
let cp2 = checkpoint_for(&owner_store, topic, &kp, 2);
let mut full2 = owner_store.full_delta();
full2.owner_checkpoint = Some(cp2);
joiner
.merge_delta(&full2, peer(9), Some(&agent(9)))
.expect("full relay adopt");
assert!(joiner.get("k2").is_some());
assert_eq!(joiner.highest_checkpoint_seq, 2);
}
fn snapshot(s: &KvStore) -> Vec<(String, Vec<u8>, [u8; 32])> {
let active = s.active_entries();
let mut v: Vec<_> = active
.iter()
.map(|e| (e.key.clone(), e.value.clone(), e.content_hash))
.collect();
v.sort_by(|a, b| a.0.cmp(&b.0));
v
}
#[allow(clippy::too_many_arguments)]
fn owner_put_delta(
owner: &mut KvStore,
key: &str,
value: &[u8],
content_type: &str,
topic: &str,
kp: &crate::identity::AgentKeypair,
seq: u64,
p: PeerId,
) -> KvStoreDelta {
owner
.put(key.to_string(), value.to_vec(), content_type.to_string(), p)
.expect("owner put");
let entry = owner.get(key).cloned().expect("entry readable after put");
let version = owner.current_version();
let mut delta =
KvStoreDelta::for_put(key.to_string(), entry, (p, owner.next_seq()), version);
delta.owner_checkpoint = Some(checkpoint_for(owner, topic, kp, seq));
delta.name_update = Some(owner.name_register().clone());
delta
}
fn owner_remove_delta(
owner: &mut KvStore,
key: &str,
topic: &str,
kp: &crate::identity::AgentKeypair,
seq: u64,
) -> KvStoreDelta {
owner.remove(key).expect("owner remove");
let mut d = KvStoreDelta::new(owner.current_version());
d.removed
.insert(key.to_string(), std::collections::HashSet::new());
d.owner_checkpoint = Some(checkpoint_for(owner, topic, kp, seq));
d.name_update = Some(owner.name_register().clone());
d
}
#[test]
fn relay_forgery_per_field_tamper_rejected() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/forgery";
let id = KvStoreId::for_topic_owner(topic, &owner);
let mut owner_store = KvStore::new(id, "Legit".to_string(), owner, AccessPolicy::Signed);
owner_store
.put(
"k".to_string(),
b"v".to_vec(),
"text/plain".to_string(),
peer(1),
)
.expect("owner put");
let cp = checkpoint_for(&owner_store, topic, &kp, 1);
{
let mut legit = owner_store.full_delta();
legit.owner_checkpoint = Some(cp.clone());
let mut j =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
j.merge_delta(&legit, peer(9), Some(&agent(9)))
.expect("legit adopt");
assert!(j.get("k").is_some(), "legit relay must adopt");
assert_eq!(j.highest_checkpoint_seq, 1);
}
#[allow(clippy::type_complexity)]
let mutators: Vec<(&str, Box<dyn FnOnce(&mut KvStoreDelta)>)> = vec![
(
"value_swap_unchanged_hash",
Box::new(|d| {
d.added.get_mut("k").unwrap().0.value = b"PWNED".to_vec();
}),
),
(
"content_type",
Box::new(|d| {
d.added.get_mut("k").unwrap().0.content_type = "evil/x".to_string();
}),
),
(
"metadata",
Box::new(|d| {
d.added
.get_mut("k")
.unwrap()
.0
.metadata
.insert("injected".to_string(), "yes".to_string());
}),
),
(
"created_at",
Box::new(|d| {
d.added.get_mut("k").unwrap().0.created_at = 0;
}),
),
(
"updated_at",
Box::new(|d| {
d.added.get_mut("k").unwrap().0.updated_at = 0;
}),
),
(
"outer_key",
Box::new(|d| {
let pair = d.added.remove("k").unwrap();
d.added.insert("k2".to_string(), pair);
}),
),
(
"inner_key",
Box::new(|d| {
d.added.get_mut("k").unwrap().0.key = "k2".to_string();
}),
),
(
"store_name",
Box::new(|d| {
d.name_update
.as_mut()
.unwrap()
.set("EVIL".to_string(), peer(9));
}),
),
];
for (name, mutate) in mutators {
let mut delta = owner_store.full_delta();
delta.owner_checkpoint = Some(cp.clone());
mutate(&mut delta);
let mut joiner =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
joiner
.merge_delta(&delta, peer(9), Some(&agent(9)))
.expect("silent rejection never errors");
assert!(
joiner.get("k").is_none(),
"{name}: forged entry not adopted"
);
assert!(
joiner.get("k2").is_none(),
"{name}: forged outer-key entry not adopted"
);
assert!(joiner.entries.is_empty(), "{name}: entries untouched");
assert_eq!(
*joiner.policy(),
AccessPolicy::Signed,
"{name}: policy unchanged"
);
assert!(
joiner.latest_checkpoint.is_none(),
"{name}: checkpoint cache empty"
);
assert_eq!(
joiner.highest_checkpoint_seq, 0,
"{name}: high-water mark unchanged"
);
assert_ne!(joiner.name(), "EVIL", "{name}: store name not forged");
}
}
#[test]
fn relay_matches_owner_through_put_update_delete_sequence() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/relay-exact";
let id = KvStoreId::for_topic_owner(topic, &owner);
let p = peer(1);
let mut owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
let mut relay =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
let d1 = owner_put_delta(
&mut owner_store,
"k1",
b"v1",
"text/plain",
topic,
&kp,
1,
p,
);
relay.merge_delta(&d1, p, Some(&owner)).expect("relay k1");
assert_eq!(snapshot(&owner_store), snapshot(&relay), "after k1");
assert_eq!(
relay.highest_checkpoint_seq, 1,
"seq1 caches on a fresh replica (name propagated via the incremental delta)"
);
assert_eq!(
relay.name(),
"S",
"owner's real name propagated to the fresh replica"
);
let d2 = owner_put_delta(
&mut owner_store,
"k2",
b"v2",
"text/plain",
topic,
&kp,
2,
p,
);
relay.merge_delta(&d2, p, Some(&owner)).expect("relay k2");
assert_eq!(snapshot(&owner_store), snapshot(&relay), "after k2");
assert_eq!(
relay.highest_checkpoint_seq, 2,
"relay cached cp2 after incremental write (maybe_cache_checkpoint)"
);
let d3 = owner_put_delta(
&mut owner_store,
"k1",
b"v1b",
"application/json",
topic,
&kp,
3,
p,
);
relay
.merge_delta(&d3, p, Some(&owner))
.expect("relay update k1");
assert_eq!(snapshot(&owner_store), snapshot(&relay), "after update k1");
assert_eq!(relay.highest_checkpoint_seq, 3, "relay cached cp3");
let d4 = owner_remove_delta(&mut owner_store, "k1", topic, &kp, 4);
relay
.merge_delta(&d4, p, Some(&owner))
.expect("relay delete k1");
assert_eq!(snapshot(&owner_store), snapshot(&relay), "after delete k1");
assert!(relay.get("k1").is_none());
assert_eq!(relay.highest_checkpoint_seq, 4, "relay cached cp4");
let d5 = owner_remove_delta(&mut owner_store, "k2", topic, &kp, 5);
relay
.merge_delta(&d5, p, Some(&owner))
.expect("relay delete k2");
assert_eq!(snapshot(&owner_store), snapshot(&relay), "after delete k2");
assert!(
relay.get("k2").is_none(),
"delete-to-empty reconciled on the checkpoint path"
);
assert!(
relay.active_entries().is_empty(),
"relay is empty, exactly matching the owner"
);
assert_eq!(relay.highest_checkpoint_seq, 5, "relay cached cp5");
}
#[test]
fn offline_anchored_joiner_recovers_multikey_state_from_relay() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/offline-recovery";
let id = KvStoreId::for_topic_owner(topic, &owner);
let p = peer(1);
let mut owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
let mut relay =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
let d1 = owner_put_delta(
&mut owner_store,
"k1",
b"v1",
"text/plain",
topic,
&kp,
1,
p,
);
relay.merge_delta(&d1, p, Some(&owner)).expect("relay k1");
let d2 = owner_put_delta(
&mut owner_store,
"k2",
b"v2",
"text/plain",
topic,
&kp,
2,
p,
);
relay.merge_delta(&d2, p, Some(&owner)).expect("relay k2");
assert_eq!(
relay.highest_checkpoint_seq, 2,
"relay cached the two-key checkpoint"
);
let relay_full = relay.full_delta();
assert!(
relay_full.owner_checkpoint.is_some(),
"relay carries a cached owner checkpoint for cold recovery"
);
let mut joiner =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
joiner
.merge_delta(&relay_full, peer(9), Some(&agent(9)))
.expect("cold-recovery merge");
assert_eq!(
snapshot(&joiner),
snapshot(&owner_store),
"joiner recovers the exact owner state from the relay"
);
assert_eq!(joiner.highest_checkpoint_seq, 2);
}
#[test]
fn checkpoint_high_water_mark_survives_restart_and_rejects_replay() {
let kp = crate::identity::AgentKeypair::generate().expect("keypair");
let owner = kp.agent_id();
let topic = "store/restart-replay";
let id = KvStoreId::for_topic_owner(topic, &owner);
let p = peer(1);
let mut owner_store = KvStore::new(id, "S".to_string(), owner, AccessPolicy::Signed);
owner_store
.put("k".to_string(), b"v".to_vec(), "text/plain".to_string(), p)
.expect("owner put");
let cp1 = checkpoint_for(&owner_store, topic, &kp, 1);
let mut snap1 = owner_store.full_delta();
snap1.owner_checkpoint = Some(cp1);
let mut replica =
KvStore::new_replica(id, String::new(), Some(owner), AnchorChannel::RestParam);
replica
.merge_delta(&snap1, peer(9), Some(&agent(9)))
.expect("adopt full snapshot");
assert_eq!(replica.highest_checkpoint_seq, 1);
assert!(replica.latest_checkpoint.is_some());
assert!(replica.get("k").is_some());
let bytes = bincode::serialize(&replica).expect("serialize");
let mut restarted: KvStore = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(
restarted.highest_checkpoint_seq, 1,
"highest_checkpoint_seq persisted across restart (no longer serde(skip))"
);
assert!(
restarted.latest_checkpoint.is_some(),
"latest_checkpoint persisted across restart"
);
let v_before = restarted.current_version();
restarted
.merge_delta(&snap1, peer(9), Some(&agent(9)))
.expect("no error");
assert_eq!(
restarted.current_version(),
v_before,
"replay of already-adopted checkpoint must not mutate after restart"
);
assert_eq!(restarted.highest_checkpoint_seq, 1);
owner_store
.put(
"k2".to_string(),
b"v2".to_vec(),
"text/plain".to_string(),
p,
)
.expect("owner put k2");
let cp2 = checkpoint_for(&owner_store, topic, &kp, 2);
let mut snap2 = owner_store.full_delta();
snap2.owner_checkpoint = Some(cp2);
restarted
.merge_delta(&snap2, peer(9), Some(&agent(9)))
.expect("adopt newer");
assert_eq!(
restarted.highest_checkpoint_seq, 2,
"newer checkpoint advances"
);
assert!(restarted.get("k2").is_some());
}
}