use crate::error::LiteError;
use crate::identity::peer_id::{is_valid_peer_id, mint_peer_id};
use crate::storage::engine::StorageEngine;
const LITE_ID_KEY: &[u8] = b"meta:lite_id";
const EPOCH_KEY: &[u8] = b"meta:epoch";
const PEER_ID_KEY: &[u8] = b"meta:peer_id";
#[derive(Debug, Clone)]
pub struct LiteIdentity {
pub lite_id: String,
pub epoch: u64,
pub peer_id: u64,
}
impl LiteIdentity {
pub async fn load_or_create<S: StorageEngine>(storage: &S) -> Result<Self, LiteError> {
let ns = nodedb_types::Namespace::Meta;
let lite_id = match storage.get(ns, LITE_ID_KEY).await? {
Some(bytes) => {
String::from_utf8(bytes).unwrap_or_else(|_| nodedb_types::id_gen::uuid_v7())
}
None => {
let id = nodedb_types::id_gen::uuid_v7();
storage.put(ns, LITE_ID_KEY, id.as_bytes()).await?;
id
}
};
let stored_peer_id = match storage.get(ns, PEER_ID_KEY).await? {
Some(bytes) if bytes.len() == 8 => {
let id = u64::from_le_bytes(bytes.try_into().unwrap_or([0; 8]));
is_valid_peer_id(id).then_some(id)
}
_ => None,
};
let peer_id = match stored_peer_id {
Some(id) => id,
None => {
let id = mint_peer_id();
storage.put(ns, PEER_ID_KEY, &id.to_le_bytes()).await?;
id
}
};
let epoch = match storage.get(ns, EPOCH_KEY).await? {
Some(bytes) if bytes.len() == 8 => {
let prev = u64::from_le_bytes(bytes.try_into().unwrap_or([0; 8]));
prev + 1
}
_ => 1,
};
storage.put(ns, EPOCH_KEY, &epoch.to_le_bytes()).await?;
Ok(Self {
lite_id,
epoch,
peer_id,
})
}
pub async fn regenerate<S: StorageEngine>(&mut self, storage: &S) -> Result<(), LiteError> {
let ns = nodedb_types::Namespace::Meta;
self.lite_id = nodedb_types::id_gen::uuid_v7();
self.epoch = 1;
storage
.put(ns, LITE_ID_KEY, self.lite_id.as_bytes())
.await?;
storage
.put(ns, EPOCH_KEY, &self.epoch.to_le_bytes())
.await?;
self.rotate_peer_id(storage).await?;
Ok(())
}
pub async fn rotate_peer_id<S: StorageEngine>(
&mut self,
storage: &S,
) -> Result<u64, LiteError> {
let ns = nodedb_types::Namespace::Meta;
let previous = self.peer_id;
let mut next = mint_peer_id();
while next == previous {
next = mint_peer_id();
}
storage.put(ns, PEER_ID_KEY, &next.to_le_bytes()).await?;
self.peer_id = next;
Ok(next)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::peer_id::is_valid_peer_id;
use crate::storage::pagedb_storage::PagedbStorageMem;
#[tokio::test]
async fn first_open_creates_identity() {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
let identity = LiteIdentity::load_or_create(&storage).await.unwrap();
assert!(!identity.lite_id.is_empty());
assert_eq!(identity.epoch, 1);
assert!(is_valid_peer_id(identity.peer_id));
}
#[tokio::test]
async fn second_open_increments_epoch() {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
let id1 = LiteIdentity::load_or_create(&storage).await.unwrap();
let id2 = LiteIdentity::load_or_create(&storage).await.unwrap();
assert_eq!(id1.lite_id, id2.lite_id); assert_eq!(id2.epoch, 2); }
#[tokio::test]
async fn peer_id_is_stable_across_opens() {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
let id1 = LiteIdentity::load_or_create(&storage).await.unwrap();
let id2 = LiteIdentity::load_or_create(&storage).await.unwrap();
assert_eq!(
id1.peer_id, id2.peer_id,
"reopening must not change who authored the store's existing operations"
);
}
#[tokio::test]
async fn separate_stores_mint_separate_peer_ids() {
let a = PagedbStorageMem::open_in_memory().await.unwrap();
let b = PagedbStorageMem::open_in_memory().await.unwrap();
let id_a = LiteIdentity::load_or_create(&a).await.unwrap();
let id_b = LiteIdentity::load_or_create(&b).await.unwrap();
assert_ne!(id_a.peer_id, id_b.peer_id);
assert_ne!(id_a.lite_id, id_b.lite_id);
}
#[tokio::test]
async fn rotate_peer_id_persists_and_leaves_the_producer_identity() {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
let mut identity = LiteIdentity::load_or_create(&storage).await.unwrap();
let (lite_id, epoch, before) = (identity.lite_id.clone(), identity.epoch, identity.peer_id);
let rotated = identity.rotate_peer_id(&storage).await.unwrap();
assert_ne!(rotated, before);
assert_eq!(identity.peer_id, rotated);
assert_eq!(identity.lite_id, lite_id, "rotation is not a new producer");
assert_eq!(identity.epoch, epoch);
let reloaded = LiteIdentity::load_or_create(&storage).await.unwrap();
assert_eq!(
reloaded.peer_id, rotated,
"a rotation that is forgotten on restart resumes the refused id"
);
}
#[tokio::test]
async fn regenerate_changes_id_and_peer_id() {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
let mut id = LiteIdentity::load_or_create(&storage).await.unwrap();
let original_id = id.lite_id.clone();
let original_peer = id.peer_id;
id.regenerate(&storage).await.unwrap();
assert_ne!(id.lite_id, original_id);
assert_ne!(
id.peer_id, original_peer,
"a forked store must stop authoring under the forked peer id"
);
assert_eq!(id.epoch, 1);
}
#[tokio::test]
async fn invalid_stored_peer_id_is_reminted() {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
storage
.put(
nodedb_types::Namespace::Meta,
PEER_ID_KEY,
&0u64.to_le_bytes(),
)
.await
.unwrap();
let identity = LiteIdentity::load_or_create(&storage).await.unwrap();
assert!(
is_valid_peer_id(identity.peer_id),
"Loro reads 0 as unset; it must never reach the document"
);
}
}