feat: introduce global meta store and root store concept, and update CLI to manage active store

This commit is contained in:
2025-12-21 23:59:08 +01:00
parent f45c6ccfcf
commit 346ebccee7
15 changed files with 851 additions and 283 deletions
+88 -14
View File
@@ -23,6 +23,9 @@ pub enum SigChainError {
#[error("Wrong author: expected {expected}, got {got}")]
WrongAuthor { expected: String, got: String },
#[error("Wrong store_id: expected {expected}, got {got}")]
WrongStoreId { expected: String, got: String },
#[error("Invalid sequence: expected {expected}, got {got}")]
InvalidSequence { expected: u64, got: u64 },
@@ -34,11 +37,14 @@ pub enum SigChainError {
}
/// An append-only log where each entry is cryptographically signed
/// and hash-linked to the previous entry.
/// and hash-linked to the previous entry, scoped to a specific store.
pub struct SigChain {
/// Path to the log file
log_path: PathBuf,
/// Store UUID (16 bytes)
store_id: [u8; 16],
/// Author's public key (32 bytes)
author_id: [u8; 32],
@@ -50,10 +56,11 @@ pub struct SigChain {
}
impl SigChain {
/// Create a new empty sigchain for an author
pub fn new(log_path: impl AsRef<Path>, author_id: [u8; 32]) -> Self {
/// Create a new empty sigchain for a (store, author) pair
pub fn new(log_path: impl AsRef<Path>, store_id: [u8; 16], author_id: [u8; 32]) -> Self {
Self {
log_path: log_path.as_ref().to_path_buf(),
store_id,
author_id,
next_seq: 1,
last_hash: [0u8; 32],
@@ -61,11 +68,11 @@ impl SigChain {
}
/// Load a sigchain from an existing log file
pub fn from_log(log_path: impl AsRef<Path>, author_id: [u8; 32]) -> Result<Self, SigChainError> {
pub fn from_log(log_path: impl AsRef<Path>, store_id: [u8; 16], author_id: [u8; 32]) -> Result<Self, SigChainError> {
let log_path = log_path.as_ref().to_path_buf();
let entries = read_entries(&log_path)?;
let mut chain = Self::new(&log_path, author_id);
let mut chain = Self::new(&log_path, store_id, author_id);
for signed_entry in entries {
// Verify signature
@@ -85,6 +92,18 @@ impl SigChain {
// Decode Entry
let entry = Entry::decode(&signed_entry.entry_bytes[..])?;
// Validate store_id
// Note: Empty/malformed store_id becomes [0u8;16], which fails validation
// against any real UUID store. This intentionally rejects legacy entries.
let entry_store: [u8; 16] = entry.store_id.clone().try_into()
.unwrap_or([0u8; 16]);
if entry_store != store_id {
return Err(SigChainError::WrongStoreId {
expected: hex::encode(store_id),
got: hex::encode(entry_store),
});
}
// Validate sequence
if entry.seq != chain.next_seq {
return Err(SigChainError::InvalidSequence {
@@ -156,6 +175,18 @@ impl SigChain {
// Decode entry
let entry = Entry::decode(&signed_entry.entry_bytes[..])?;
// Validate store_id
// Note: Empty/malformed store_id becomes [0u8;16], which fails validation
// against any real UUID store. This intentionally rejects legacy entries.
let entry_store: [u8; 16] = entry.store_id.clone().try_into()
.unwrap_or([0u8; 16]);
if entry_store != self.store_id {
return Err(SigChainError::WrongStoreId {
expected: hex::encode(self.store_id),
got: hex::encode(entry_store),
});
}
// Validate sequence
if entry.seq != self.next_seq {
return Err(SigChainError::InvalidSequence {
@@ -201,6 +232,7 @@ impl SigChain {
let hlc = HLC::now_with_clock(&SystemClock);
let mut builder = EntryBuilder::new(self.next_seq, hlc)
.store_id(self.store_id.to_vec())
.prev_hash(self.last_hash.to_vec());
// Add operations
@@ -230,12 +262,14 @@ mod tests {
temp_dir().join(format!("lattice_sigchain_test_{}.log", name))
}
const TEST_STORE: [u8; 16] = [1u8; 16];
#[test]
fn test_new_sigchain() {
let path = temp_log_path("new");
let author = [1u8; 32];
let chain = SigChain::new(&path, author);
let chain = SigChain::new(&path, TEST_STORE, author);
assert_eq!(chain.author_id(), &author);
assert_eq!(chain.next_seq(), 1);
@@ -251,10 +285,11 @@ mod tests {
let node = Node::generate();
let author = node.public_key_bytes();
let mut chain = SigChain::new(&path, author);
let mut chain = SigChain::new(&path, TEST_STORE, author);
let clock = MockClock::new(1000);
let entry = EntryBuilder::new(1, HLC::now_with_clock(&clock))
.store_id(TEST_STORE.to_vec())
.prev_hash([0u8; 32].to_vec())
.put("/key", b"value".to_vec())
.sign(&node);
@@ -275,11 +310,12 @@ mod tests {
let node = Node::generate();
let author = node.public_key_bytes();
let mut chain = SigChain::new(&path, author);
let mut chain = SigChain::new(&path, TEST_STORE, author);
let clock = MockClock::new(1000);
for i in 1..=3 {
let entry = EntryBuilder::new(i, HLC::now_with_clock(&clock))
.store_id(TEST_STORE.to_vec())
.prev_hash(chain.last_hash.to_vec())
.put(format!("/key/{}", i), format!("value{}", i).into_bytes())
.sign(&node);
@@ -303,9 +339,10 @@ mod tests {
// Write some entries
{
let mut chain = SigChain::new(&path, author);
let mut chain = SigChain::new(&path, TEST_STORE, author);
for i in 1..=3 {
let entry = EntryBuilder::new(i, HLC::now_with_clock(&clock))
.store_id(TEST_STORE.to_vec())
.prev_hash(chain.last_hash.to_vec())
.put("/key", b"val".to_vec())
.sign(&node);
@@ -314,7 +351,7 @@ mod tests {
}
// Reload from log
let chain = SigChain::from_log(&path, author).unwrap();
let chain = SigChain::from_log(&path, TEST_STORE, author).unwrap();
assert_eq!(chain.len(), 3);
assert_eq!(chain.next_seq(), 4);
@@ -329,11 +366,12 @@ mod tests {
let node = Node::generate();
let author = node.public_key_bytes();
let mut chain = SigChain::new(&path, author);
let mut chain = SigChain::new(&path, TEST_STORE, author);
let clock = MockClock::new(1000);
// Try to append with wrong seq (2 instead of 1)
let entry = EntryBuilder::new(2, HLC::now_with_clock(&clock))
.store_id(TEST_STORE.to_vec())
.prev_hash([0u8; 32].to_vec())
.put("/key", b"val".to_vec())
.sign(&node);
@@ -352,11 +390,12 @@ mod tests {
let node = Node::generate();
let author = node.public_key_bytes();
let mut chain = SigChain::new(&path, author);
let mut chain = SigChain::new(&path, TEST_STORE, author);
let clock = MockClock::new(1000);
// First entry
let entry1 = EntryBuilder::new(1, HLC::now_with_clock(&clock))
.store_id(TEST_STORE.to_vec())
.prev_hash([0u8; 32].to_vec())
.put("/key", b"v1".to_vec())
.sign(&node);
@@ -364,6 +403,7 @@ mod tests {
// Second entry with wrong prev_hash
let entry2 = EntryBuilder::new(2, HLC::now_with_clock(&clock))
.store_id(TEST_STORE.to_vec())
.prev_hash([99u8; 32].to_vec()) // Wrong!
.put("/key", b"v2".to_vec())
.sign(&node);
@@ -382,11 +422,12 @@ mod tests {
let node = Node::generate();
let other_author = [99u8; 32]; // Different author
let mut chain = SigChain::new(&path, other_author);
let mut chain = SigChain::new(&path, TEST_STORE, other_author);
let clock = MockClock::new(1000);
// Entry signed by node but chain expects other_author
let entry = EntryBuilder::new(1, HLC::now_with_clock(&clock))
.store_id(TEST_STORE.to_vec())
.prev_hash([0u8; 32].to_vec())
.put("/key", b"val".to_vec())
.sign(&node);
@@ -405,7 +446,7 @@ mod tests {
let node = Node::generate();
let author = node.public_key_bytes();
let mut chain = SigChain::new(&path, author);
let mut chain = SigChain::new(&path, TEST_STORE, author);
let ops = vec![
Operation {
@@ -427,4 +468,37 @@ mod tests {
std::fs::remove_file(&path).ok();
}
#[test]
fn test_reject_wrong_store_id() {
let path_a = temp_log_path("storeid_a");
let path_b = temp_log_path("storeid_b");
std::fs::remove_file(&path_a).ok();
std::fs::remove_file(&path_b).ok();
let node = Node::generate();
let author = node.public_key_bytes();
let clock = MockClock::new(1000);
let store_a = [0xAAu8; 16];
let store_b = [0xBBu8; 16];
// Create valid entry for store A
let mut chain_a = SigChain::new(&path_a, store_a, author);
let entry = EntryBuilder::new(1, HLC::now_with_clock(&clock))
.store_id(store_a.to_vec())
.prev_hash([0u8; 32].to_vec())
.put("/key", b"val".to_vec())
.sign(&node);
chain_a.append(&entry).unwrap();
// Try to replay that entry into store B's chain
let mut chain_b = SigChain::new(&path_b, store_b, author);
let result = chain_b.append(&entry);
assert!(matches!(result, Err(SigChainError::WrongStoreId { .. })));
std::fs::remove_file(&path_a).ok();
std::fs::remove_file(&path_b).ok();
}
}