feat: Implement DAG-based conflict resolution with binary keys and multi-head CLI display
This commit is contained in:
@@ -303,4 +303,7 @@ Read permissions are not enforceable:
|
|||||||
Practical model:
|
Practical model:
|
||||||
- Share store = grant read
|
- Share store = grant read
|
||||||
- Write access defined in manifest
|
- Write access defined in manifest
|
||||||
- Read-only nodes replicate and verify but can't contribute entries
|
- Read-only nodes replicate and verify but can't contribute entries
|
||||||
|
|
||||||
|
Future:
|
||||||
|
- Capability-based permissions: Explore finer-grained write access (e.g., per-key or per-prefix permissions) via capabilities. Exact mechanism TBD.
|
||||||
+13
-12
@@ -33,26 +33,27 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 1.5: DAG Conflict Resolution ← NEXT
|
## Milestone 1.5: DAG Conflict Resolution
|
||||||
|
|
||||||
**Goal:** Upgrade store from simple LWW to DAG-based conflict resolution per architecture.md.
|
**Goal:** Upgrade store from simple LWW to DAG-based conflict resolution per architecture.md.
|
||||||
|
|
||||||
### Deliverables
|
### Deliverables
|
||||||
|
|
||||||
- [ ] Proto: Add `repeated bytes parent_hashes` to Entry (for DAG causality, separate from sigchain `prev_hash`)
|
- [x] Proto: Add `repeated bytes parent_hashes` to Entry (for DAG causality)
|
||||||
- [ ] Store: keys → `Vec<u8>` (binary, not String)
|
- [x] Proto: Add `HeadInfo` message for multi-head storage
|
||||||
- [ ] Store: KV table schema → `Vec<u8> → Vec<HeadInfo>` where `HeadInfo = { value, hlc, author, hash }`
|
- [x] Store: KV table schema → `Vec<u8> → Vec<HeadInfo>`
|
||||||
- [ ] Store: `applied_frontiers` table → `author_id → (seq, hash)` per author
|
- [x] Store: `apply_entry` → track multiple heads, merge parent tips
|
||||||
- [ ] Store: `apply_entry` → track multiple heads, merge parent tips into new tip
|
- [x] Store: `get` → deterministic winner (highest HLC, author tiebreaker)
|
||||||
- [ ] Store: `get` → deterministic winner from heads (highest HLC, author_id tiebreaker)
|
- [x] Store: `get_heads` → inspect all heads for a key
|
||||||
- [ ] EntryBuilder: `.parent_hashes(...)` method for DAG ancestry
|
- [x] EntryBuilder: `.parent_hashes(...)` method for DAG ancestry
|
||||||
|
- [x] CLI: Show conflict indicator when multiple heads
|
||||||
|
|
||||||
### Success Criteria
|
### Success Criteria
|
||||||
|
|
||||||
- Concurrent writes to same key create multiple heads
|
- [x] Concurrent writes to same key create multiple heads
|
||||||
- Reads return deterministic winner
|
- [x] Reads return deterministic winner
|
||||||
- Next write citing both heads merges fork to single tip
|
- [x] Next write citing both heads merges fork to single tip
|
||||||
- All existing tests still pass
|
- [x] All existing tests still pass (71 tests)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+60
-12
@@ -67,10 +67,10 @@ pub fn commands() -> Vec<Command> {
|
|||||||
},
|
},
|
||||||
Command {
|
Command {
|
||||||
name: "get",
|
name: "get",
|
||||||
args: "<key>",
|
args: "<key> [-v]",
|
||||||
description: "Retrieve a value by key",
|
description: "Retrieve a value by key",
|
||||||
min_args: 1,
|
min_args: 1,
|
||||||
max_args: 1,
|
max_args: 2,
|
||||||
handler: cmd_get,
|
handler: cmd_get,
|
||||||
},
|
},
|
||||||
Command {
|
Command {
|
||||||
@@ -240,7 +240,7 @@ fn cmd_put(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) ->
|
|||||||
return CommandResult::Ok;
|
return CommandResult::Ok;
|
||||||
};
|
};
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
match h.put(&args[0], args[1].as_bytes()) {
|
match h.put(args[0].as_bytes(), args[1].as_bytes()) {
|
||||||
Ok(seq) => println!("OK (seq: {}, {:.2?})", seq, start.elapsed()),
|
Ok(seq) => println!("OK (seq: {}, {:.2?})", seq, start.elapsed()),
|
||||||
Err(e) => eprintln!("Error: {}", e),
|
Err(e) => eprintln!("Error: {}", e),
|
||||||
}
|
}
|
||||||
@@ -252,14 +252,48 @@ fn cmd_get(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) ->
|
|||||||
println!("No store selected. Use 'init' or 'use <uuid>'");
|
println!("No store selected. Use 'init' or 'use <uuid>'");
|
||||||
return CommandResult::Ok;
|
return CommandResult::Ok;
|
||||||
};
|
};
|
||||||
|
let verbose = args.get(1).map(|a| a == "-v").unwrap_or(false);
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
match h.get(&args[0]) {
|
let key = args[0].as_bytes();
|
||||||
Ok(Some(v)) => {
|
|
||||||
println!("{}", format_value(&v));
|
if verbose {
|
||||||
println!("({:.2?})", start.elapsed());
|
// Show all heads
|
||||||
|
match h.get_heads(key) {
|
||||||
|
Ok(heads) if heads.is_empty() => println!("(nil)"),
|
||||||
|
Ok(heads) => {
|
||||||
|
for (i, head) in heads.iter().enumerate() {
|
||||||
|
let winner = if i == 0 { "→" } else { " " };
|
||||||
|
let tombstone = if head.tombstone { "⊗" } else { "" };
|
||||||
|
let author_short = hex::encode(&head.author).chars().take(8).collect::<String>();
|
||||||
|
if head.tombstone {
|
||||||
|
println!("{} {} (deleted) (hlc:{}, author:{})",
|
||||||
|
winner, tombstone, head.hlc, author_short);
|
||||||
|
} else {
|
||||||
|
println!("{} {} (hlc:{}, author:{})",
|
||||||
|
winner, format_value(&head.value), head.hlc, author_short);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if heads.len() > 1 {
|
||||||
|
println!("⚠ {} heads (conflict)", heads.len());
|
||||||
|
}
|
||||||
|
println!("({:.2?})", start.elapsed());
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Error: {}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match h.get(key) {
|
||||||
|
Ok(Some(v)) => {
|
||||||
|
let heads = h.get_heads(key).unwrap_or_default();
|
||||||
|
if heads.len() > 1 {
|
||||||
|
println!("{} (⚠ {} heads)", format_value(&v), heads.len());
|
||||||
|
} else {
|
||||||
|
println!("{}", format_value(&v));
|
||||||
|
}
|
||||||
|
println!("({:.2?})", start.elapsed());
|
||||||
|
}
|
||||||
|
Ok(None) => println!("(nil)"),
|
||||||
|
Err(e) => eprintln!("Error: {}", e),
|
||||||
}
|
}
|
||||||
Ok(None) => println!("(nil)"),
|
|
||||||
Err(e) => eprintln!("Error: {}", e),
|
|
||||||
}
|
}
|
||||||
CommandResult::Ok
|
CommandResult::Ok
|
||||||
}
|
}
|
||||||
@@ -270,7 +304,7 @@ fn cmd_delete(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String])
|
|||||||
return CommandResult::Ok;
|
return CommandResult::Ok;
|
||||||
};
|
};
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
match h.delete(&args[0]) {
|
match h.delete(args[0].as_bytes()) {
|
||||||
Ok(seq) => println!("OK (seq: {}, {:.2?})", seq, start.elapsed()),
|
Ok(seq) => println!("OK (seq: {}, {:.2?})", seq, start.elapsed()),
|
||||||
Err(e) => eprintln!("Error: {}", e),
|
Err(e) => eprintln!("Error: {}", e),
|
||||||
}
|
}
|
||||||
@@ -290,10 +324,24 @@ fn cmd_list(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) -
|
|||||||
println!("(empty)");
|
println!("(empty)");
|
||||||
} else {
|
} else {
|
||||||
for (k, v) in &entries {
|
for (k, v) in &entries {
|
||||||
|
let key_str = format_value(k);
|
||||||
if verbose {
|
if verbose {
|
||||||
println!("{} = {} ({} bytes)", k, format_value(v), v.len());
|
// Show all heads for this key
|
||||||
|
let heads = h.get_heads(k).unwrap_or_default();
|
||||||
|
println!("{}:", key_str);
|
||||||
|
for (i, head) in heads.iter().enumerate() {
|
||||||
|
let winner = if i == 0 { "→" } else { " " };
|
||||||
|
let author_short = hex::encode(&head.author).chars().take(8).collect::<String>();
|
||||||
|
if head.tombstone {
|
||||||
|
println!(" {} ⊗ (deleted) (hlc:{}, author:{})",
|
||||||
|
winner, head.hlc, author_short);
|
||||||
|
} else {
|
||||||
|
println!(" {} {} (hlc:{}, author:{})",
|
||||||
|
winner, format_value(&head.value), head.hlc, author_short);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("{} = {}", k, format_value(v));
|
println!("{} = {}", key_str, format_value(v));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("({} keys, {:.2?})", entries.len(), start.elapsed());
|
println!("({} keys, {:.2?})", entries.len(), start.elapsed());
|
||||||
|
|||||||
+30
-14
@@ -189,11 +189,15 @@ pub struct StoreHandle {
|
|||||||
impl StoreHandle {
|
impl StoreHandle {
|
||||||
pub fn id(&self) -> Uuid { self.store_id }
|
pub fn id(&self) -> Uuid { self.store_id }
|
||||||
|
|
||||||
pub fn get(&self, key: &str) -> Result<Option<Vec<u8>>, NodeError> {
|
pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, NodeError> {
|
||||||
Ok(self.store.get(key)?)
|
Ok(self.store.get(key)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(&self) -> Result<Vec<(String, Vec<u8>)>, NodeError> {
|
pub fn get_heads(&self, key: &[u8]) -> Result<Vec<lattice_core::HeadInfo>, NodeError> {
|
||||||
|
Ok(self.store.get_heads(key)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> Result<Vec<(Vec<u8>, Vec<u8>)>, NodeError> {
|
||||||
Ok(self.store.list_all()?)
|
Ok(self.store.list_all()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,18 +206,29 @@ impl StoreHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn applied_seq(&self) -> Result<u64, NodeError> {
|
pub fn applied_seq(&self) -> Result<u64, NodeError> {
|
||||||
Ok(self.store.last_seq()?)
|
let author = self.node.public_key_bytes();
|
||||||
|
Ok(self.store.author_state(&author)?
|
||||||
|
.map(|s| s.seq)
|
||||||
|
.unwrap_or(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put(&self, key: &str, value: &[u8]) -> Result<u64, NodeError> {
|
pub fn put(&self, key: &[u8], value: &[u8]) -> Result<u64, NodeError> {
|
||||||
self.commit_entry(|b| b.put(key, value.to_vec()))
|
// Get current heads for this key to cite as parents
|
||||||
|
let heads = self.store.get_heads(key)?;
|
||||||
|
let parent_hashes: Vec<Vec<u8>> = heads.iter().map(|h| h.hash.clone()).collect();
|
||||||
|
|
||||||
|
self.commit_entry(parent_hashes, |b| b.put(key.to_vec(), value.to_vec()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, key: &str) -> Result<u64, NodeError> {
|
pub fn delete(&self, key: &[u8]) -> Result<u64, NodeError> {
|
||||||
self.commit_entry(|b| b.delete(key))
|
// Get current heads for this key to cite as parents
|
||||||
|
let heads = self.store.get_heads(key)?;
|
||||||
|
let parent_hashes: Vec<Vec<u8>> = heads.iter().map(|h| h.hash.clone()).collect();
|
||||||
|
|
||||||
|
self.commit_entry(parent_hashes, |b| b.delete(key.to_vec()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit_entry<F>(&self, build: F) -> Result<u64, NodeError>
|
fn commit_entry<F>(&self, parent_hashes: Vec<Vec<u8>>, build: F) -> Result<u64, NodeError>
|
||||||
where
|
where
|
||||||
F: FnOnce(EntryBuilder) -> EntryBuilder,
|
F: FnOnce(EntryBuilder) -> EntryBuilder,
|
||||||
{
|
{
|
||||||
@@ -223,7 +238,8 @@ impl StoreHandle {
|
|||||||
|
|
||||||
let builder = EntryBuilder::new(seq, HLC::now())
|
let builder = EntryBuilder::new(seq, HLC::now())
|
||||||
.store_id(self.store_id.as_bytes().to_vec())
|
.store_id(self.store_id.as_bytes().to_vec())
|
||||||
.prev_hash(prev_hash.to_vec());
|
.prev_hash(prev_hash.to_vec())
|
||||||
|
.parent_hashes(parent_hashes);
|
||||||
let entry = build(builder).sign(&self.node);
|
let entry = build(builder).sign(&self.node);
|
||||||
|
|
||||||
sigchain.append(&entry)?;
|
sigchain.append(&entry)?;
|
||||||
@@ -261,8 +277,8 @@ mod tests {
|
|||||||
assert!(stores.contains(&store_id));
|
assert!(stores.contains(&store_id));
|
||||||
|
|
||||||
let (handle, _) = node.open_store(store_id).expect("Failed to open store");
|
let (handle, _) = node.open_store(store_id).expect("Failed to open store");
|
||||||
handle.put("/key", b"value").expect("put failed");
|
handle.put(b"/key", b"value").expect("put failed");
|
||||||
assert_eq!(handle.get("/key").unwrap(), Some(b"value".to_vec()));
|
assert_eq!(handle.get(b"/key").unwrap(), Some(b"value".to_vec()));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||||
}
|
}
|
||||||
@@ -279,12 +295,12 @@ mod tests {
|
|||||||
let store_b = node.create_store().expect("create B");
|
let store_b = node.create_store().expect("create B");
|
||||||
|
|
||||||
let (handle_a, _) = node.open_store(store_a).expect("open A");
|
let (handle_a, _) = node.open_store(store_a).expect("open A");
|
||||||
handle_a.put("/key", b"from A").expect("put A");
|
handle_a.put(b"/key", b"from A").expect("put A");
|
||||||
|
|
||||||
let (handle_b, _) = node.open_store(store_b).expect("open B");
|
let (handle_b, _) = node.open_store(store_b).expect("open B");
|
||||||
assert_eq!(handle_b.get("/key").unwrap(), None);
|
assert_eq!(handle_b.get(b"/key").unwrap(), None);
|
||||||
|
|
||||||
assert_eq!(handle_a.get("/key").unwrap(), Some(b"from A".to_vec()));
|
assert_eq!(handle_a.get(b"/key").unwrap(), Some(b"from A".to_vec()));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ pub use signed_entry::{EntryBuilder, sign_entry, verify_signed_entry, hash_signe
|
|||||||
pub use log::{append_entry, read_entries, LogReader};
|
pub use log::{append_entry, read_entries, LogReader};
|
||||||
pub use store::Store;
|
pub use store::Store;
|
||||||
pub use meta_store::MetaStore;
|
pub use meta_store::MetaStore;
|
||||||
|
pub use proto::HeadInfo;
|
||||||
pub use uuid::Uuid;
|
pub use uuid::Uuid;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ mod tests {
|
|||||||
version: 1,
|
version: 1,
|
||||||
store_id: vec![1u8; 16],
|
store_id: vec![1u8; 16],
|
||||||
prev_hash: vec![0u8; 32],
|
prev_hash: vec![0u8; 32],
|
||||||
|
parent_hashes: vec![],
|
||||||
seq: 5,
|
seq: 5,
|
||||||
timestamp: Some(Hlc {
|
timestamp: Some(Hlc {
|
||||||
wall_time: 1000,
|
wall_time: 1000,
|
||||||
@@ -41,7 +42,7 @@ mod tests {
|
|||||||
ops: vec![
|
ops: vec![
|
||||||
Operation {
|
Operation {
|
||||||
op_type: Some(operation::OpType::Put(PutOp {
|
op_type: Some(operation::OpType::Put(PutOp {
|
||||||
key: "/nodes/abc".to_string(),
|
key: b"/nodes/abc".to_vec(),
|
||||||
value: b"hello".to_vec(),
|
value: b"hello".to_vec(),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ mod tests {
|
|||||||
let ops = vec![
|
let ops = vec![
|
||||||
Operation {
|
Operation {
|
||||||
op_type: Some(operation::OpType::Put(PutOp {
|
op_type: Some(operation::OpType::Put(PutOp {
|
||||||
key: "/test".to_string(),
|
key: b"/test".to_vec(),
|
||||||
value: b"hello".to_vec(),
|
value: b"hello".to_vec(),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub struct EntryBuilder {
|
|||||||
version: u32,
|
version: u32,
|
||||||
store_id: Vec<u8>,
|
store_id: Vec<u8>,
|
||||||
prev_hash: Vec<u8>,
|
prev_hash: Vec<u8>,
|
||||||
|
parent_hashes: Vec<Vec<u8>>,
|
||||||
seq: u64,
|
seq: u64,
|
||||||
timestamp: HLC,
|
timestamp: HLC,
|
||||||
ops: Vec<Operation>,
|
ops: Vec<Operation>,
|
||||||
@@ -44,8 +45,9 @@ impl EntryBuilder {
|
|||||||
pub fn new(seq: u64, timestamp: HLC) -> Self {
|
pub fn new(seq: u64, timestamp: HLC) -> Self {
|
||||||
Self {
|
Self {
|
||||||
version: 1,
|
version: 1,
|
||||||
store_id: Vec::new(), // Empty = legacy single-store
|
store_id: Vec::new(),
|
||||||
prev_hash: vec![0u8; 32], // Genesis or will be set
|
prev_hash: vec![0u8; 32],
|
||||||
|
parent_hashes: Vec::new(),
|
||||||
seq,
|
seq,
|
||||||
timestamp,
|
timestamp,
|
||||||
ops: Vec::new(),
|
ops: Vec::new(),
|
||||||
@@ -58,14 +60,20 @@ impl EntryBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the previous entry hash (for chaining)
|
/// Set the previous entry hash (for sigchain linking)
|
||||||
pub fn prev_hash(mut self, hash: impl Into<Vec<u8>>) -> Self {
|
pub fn prev_hash(mut self, hash: impl Into<Vec<u8>>) -> Self {
|
||||||
self.prev_hash = hash.into();
|
self.prev_hash = hash.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the parent hashes (for DAG ancestry)
|
||||||
|
pub fn parent_hashes(mut self, hashes: Vec<Vec<u8>>) -> Self {
|
||||||
|
self.parent_hashes = hashes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a Put operation
|
/// Add a Put operation
|
||||||
pub fn put(mut self, key: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
|
pub fn put(mut self, key: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) -> Self {
|
||||||
self.ops.push(Operation {
|
self.ops.push(Operation {
|
||||||
op_type: Some(operation::OpType::Put(PutOp {
|
op_type: Some(operation::OpType::Put(PutOp {
|
||||||
key: key.into(),
|
key: key.into(),
|
||||||
@@ -76,7 +84,7 @@ impl EntryBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add a Delete operation
|
/// Add a Delete operation
|
||||||
pub fn delete(mut self, key: impl Into<String>) -> Self {
|
pub fn delete(mut self, key: impl Into<Vec<u8>>) -> Self {
|
||||||
self.ops.push(Operation {
|
self.ops.push(Operation {
|
||||||
op_type: Some(operation::OpType::Delete(DeleteOp {
|
op_type: Some(operation::OpType::Delete(DeleteOp {
|
||||||
key: key.into(),
|
key: key.into(),
|
||||||
@@ -97,6 +105,7 @@ impl EntryBuilder {
|
|||||||
version: self.version,
|
version: self.version,
|
||||||
store_id: self.store_id,
|
store_id: self.store_id,
|
||||||
prev_hash: self.prev_hash,
|
prev_hash: self.prev_hash,
|
||||||
|
parent_hashes: self.parent_hashes,
|
||||||
seq: self.seq,
|
seq: self.seq,
|
||||||
timestamp: Some(Hlc {
|
timestamp: Some(Hlc {
|
||||||
wall_time: self.timestamp.wall_time,
|
wall_time: self.timestamp.wall_time,
|
||||||
|
|||||||
+797
-190
File diff suppressed because it is too large
Load Diff
+28
-4
@@ -24,14 +24,38 @@ message Entry {
|
|||||||
bytes store_id = 6;
|
bytes store_id = 6;
|
||||||
|
|
||||||
// Ordering Metadata
|
// Ordering Metadata
|
||||||
bytes prev_hash = 2; // Link to previous entry (32 bytes)
|
bytes prev_hash = 2; // Link to previous sigchain entry (32 bytes)
|
||||||
uint64 seq = 3; // Monotonic sequence number
|
uint64 seq = 3; // Monotonic sequence number
|
||||||
HLC timestamp = 4; // Hybrid Logical Clock
|
HLC timestamp = 4; // Hybrid Logical Clock
|
||||||
|
|
||||||
|
// DAG ancestry: hashes of entries this supersedes (separate from sigchain)
|
||||||
|
repeated bytes parent_hashes = 7;
|
||||||
|
|
||||||
// The Batch of Operations
|
// The Batch of Operations
|
||||||
repeated Operation ops = 5;
|
repeated Operation ops = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HeadInfo: a tip/head in the DAG for a key
|
||||||
|
message HeadInfo {
|
||||||
|
bytes value = 1; // The value at this head
|
||||||
|
uint64 hlc = 2; // Combined HLC for ordering (wall_time_ms << 16 | counter)
|
||||||
|
bytes author = 3; // Author's public key (32 bytes)
|
||||||
|
bytes hash = 4; // Hash of the SignedEntry that created this head
|
||||||
|
bool tombstone = 5; // True if this head represents a delete
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadList: wrapper for storing multiple heads per key in state.db
|
||||||
|
message HeadList {
|
||||||
|
repeated HeadInfo heads = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorState: tracks last applied entry per author for replay optimization
|
||||||
|
message AuthorState {
|
||||||
|
uint64 seq = 1; // Last applied seq for this author's sigchain
|
||||||
|
bytes hash = 2; // Hash of last applied entry
|
||||||
|
uint64 log_offset = 3; // Byte offset in log file for fast resume
|
||||||
|
}
|
||||||
|
|
||||||
// Hybrid Logical Clock
|
// Hybrid Logical Clock
|
||||||
message HLC {
|
message HLC {
|
||||||
uint64 wall_time = 1; // Unix timestamp (ms)
|
uint64 wall_time = 1; // Unix timestamp (ms)
|
||||||
@@ -49,12 +73,12 @@ message Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message PutOp {
|
message PutOp {
|
||||||
string key = 1;
|
bytes key = 1;
|
||||||
bytes value = 2; // Raw bytes allows storing images, JSON, binary, etc.
|
bytes value = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DeleteOp {
|
message DeleteOp {
|
||||||
string key = 1;
|
bytes key = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. The Sync Handshake (Vector Clocks)
|
// 4. The Sync Handshake (Vector Clocks)
|
||||||
|
|||||||
Reference in New Issue
Block a user