1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Error types for KvStore operations.
use crate::identity::AgentId;
/// Result type for KvStore operations.
pub type Result<T> = std::result::Result<T, KvError>;
/// Errors that can occur during KvStore operations.
#[derive(Debug, thiserror::Error)]
pub enum KvError {
/// Key not found in the store.
#[error("key not found: {0}")]
KeyNotFound(String),
/// Value exceeds the maximum inline size.
#[error("value too large: {size} bytes exceeds maximum {max} bytes")]
ValueTooLarge {
/// The actual size.
size: usize,
/// The maximum allowed size.
max: usize,
},
/// Serialization error.
#[error("serialization error: {0}")]
Serialization(#[from] bincode::Error),
/// CRDT merge operation failed.
#[error("merge error: {0}")]
Merge(String),
/// Gossip layer error.
#[error("gossip error: {0}")]
Gossip(String),
/// I/O error during persistence.
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// Store IDs do not match during merge.
#[error("store ID mismatch: cannot merge stores with different IDs")]
StoreIdMismatch,
/// Unauthorized write attempt.
#[error("unauthorized: {0}")]
Unauthorized(String),
/// The store has no anchored owner.
///
/// Ownership is established ONLY at construction from trusted out-of-band
/// input (the creator, or an explicit `expected_owner` at join). A replica
/// that joined without an anchor has no owner and is permanently read-only
/// by design — the protocol never derives ownership from an untrusted
/// network announce, so there is no silent path from `None` to `Some`.
/// Policy-restricted writes (and the local-write counterpart) fail closed.
#[error("store owner unknown: no anchored owner — store is read-only; supply expected_owner at join")]
OwnerUnknown,
/// The established owner disagrees with a received ownership announcement.
///
/// Ownership is immutable once anchored. An announce whose claimed owner
/// differs from the anchored owner is a conflict (possible takeover
/// attempt or genuine misconfiguration): it is rejected, the anchored
/// owner is unchanged, and the conflict is surfaced for auditability.
#[error("ownership conflict: anchored owner {anchored} != announced owner {claimed}")]
OwnershipConflict {
/// The anchored (construction-time) owner.
anchored: AgentId,
/// The conflicting owner claimed by the announce.
claimed: AgentId,
},
/// An ownership announcement is not a valid basis for establishing or
/// refreshing ownership.
///
/// Covers: a sender that does not match the claimed owner (third-party
/// assignment), an attempt to establish ownership on a store that has none
/// (first-self-capture guard — ownership is construction-only), and a
/// stale/replayed policy refresh. The store state is unchanged.
#[error("invalid ownership token: {0}")]
OwnerTokenInvalid(String),
/// Write rejected by the `AppendOnly` policy: the key already exists and
/// existing keys are immutable — no update to different content, no
/// delete — even for the store owner. This is what makes append-only
/// event logs tamper-evident against their own author retroactively
/// rewriting history. (A re-put of byte-identical content is accepted as
/// an idempotent no-op and never raises this error.)
#[error("immutable key: {0} — append-only store; existing keys cannot be updated or deleted")]
ImmutableKey(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display_key_not_found() {
let error = KvError::KeyNotFound("mykey".to_string());
assert!(format!("{error}").contains("key not found"));
assert!(format!("{error}").contains("mykey"));
}
#[test]
fn test_error_display_value_too_large() {
let error = KvError::ValueTooLarge {
size: 100_000,
max: 65_536,
};
let display = format!("{error}");
assert!(display.contains("100000"));
assert!(display.contains("65536"));
}
#[test]
fn test_error_display_merge() {
let error = KvError::Merge("conflict".to_string());
assert!(format!("{error}").contains("merge error"));
}
#[test]
fn test_error_display_gossip() {
let error = KvError::Gossip("timeout".to_string());
assert!(format!("{error}").contains("gossip error"));
}
#[test]
fn test_error_display_immutable_key() {
let error = KvError::ImmutableKey("evt-0001".to_string());
let display = format!("{error}");
assert!(display.contains("immutable key"));
assert!(display.contains("evt-0001"));
assert!(display.contains("append-only"));
}
#[test]
fn test_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
let kv_err: KvError = io_err.into();
assert!(format!("{kv_err}").contains("I/O error"));
}
}