Skip to main content

doubleentry/model/
ids.rs

1//! Identities: [`LedgerId`], [`IdempotencyKey`], [`EntryId`], [`AccountHandle`].
2
3use core::fmt;
4use core::str::FromStr;
5
6use super::{Decode, DecodeError, Encode, Reader, Sink, ValueError, Writer, hex};
7use crate::log::hash::{TAG_ENTRY_ID, tagged};
8
9/// A ledger's identity: 1–255 bytes, each printable ASCII (`0x21–0x7E`)
10/// except `+`.
11///
12/// Usable verbatim as a C2SP checkpoint origin and note key name, by
13/// convention a domain-style name such as `example.com/acme`.
14///
15/// ```
16/// use doubleentry::LedgerId;
17///
18/// assert!(LedgerId::new("example.com/acme").is_ok());
19/// assert!(LedgerId::new("has space").is_err());
20/// ```
21#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct LedgerId(String);
23
24impl LedgerId {
25    /// The most bytes in a ledger id.
26    pub const MAX_BYTES: usize = 255;
27
28    /// Validates a ledger id.
29    ///
30    /// # Errors
31    ///
32    /// [`ValueError::Empty`], [`ValueError::TooLong`] above 255 bytes, or
33    /// [`ValueError::InvalidCharacter`] for a space, `+`, a control or a
34    /// non-ASCII character.
35    pub fn new(text: impl Into<String>) -> Result<Self, ValueError> {
36        let text = text.into();
37        if text.is_empty() {
38            return Err(ValueError::Empty);
39        }
40        if text.len() > Self::MAX_BYTES {
41            return Err(ValueError::TooLong {
42                max: Self::MAX_BYTES,
43                found: text.len(),
44            });
45        }
46        if let Some(found) = text.chars().find(|c| !matches!(c, '!'..='~') || *c == '+') {
47            return Err(ValueError::InvalidCharacter { found });
48        }
49        Ok(Self(text))
50    }
51
52    /// The id as text.
53    #[must_use]
54    pub fn as_str(&self) -> &str {
55        &self.0
56    }
57}
58
59/// The caller's identity for an entry — typically the source transaction id:
60/// 1–128 bytes of UTF-8 without control characters, compared byte-exactly
61/// (never normalised).
62///
63/// The key is committed in the entry's leaf and the [`EntryId`] is derived
64/// from it, so a retry with the same key names the same entry.
65#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct IdempotencyKey(String);
67
68impl IdempotencyKey {
69    /// The most bytes in a key.
70    pub const MAX_BYTES: usize = 128;
71
72    /// Validates a key.
73    ///
74    /// # Errors
75    ///
76    /// [`ValueError::Empty`], [`ValueError::TooLong`] above 128 bytes, or
77    /// [`ValueError::ControlCharacter`].
78    pub fn new(text: impl Into<String>) -> Result<Self, ValueError> {
79        let text = text.into();
80        if text.is_empty() {
81            return Err(ValueError::Empty);
82        }
83        if text.len() > Self::MAX_BYTES {
84            return Err(ValueError::TooLong {
85                max: Self::MAX_BYTES,
86                found: text.len(),
87            });
88        }
89        if text.chars().any(char::is_control) {
90            return Err(ValueError::ControlCharacter);
91        }
92        Ok(Self(text))
93    }
94
95    /// The key as text.
96    #[must_use]
97    pub fn as_str(&self) -> &str {
98        &self.0
99    }
100}
101
102macro_rules! text_id {
103    ($($ty:ident),*) => {$(
104        impl fmt::Display for $ty {
105            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106                super::encoding::pad(f, &self.0)
107            }
108        }
109
110        impl fmt::Debug for $ty {
111            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112                write!(f, concat!(stringify!($ty), "({:?})"), self.0)
113            }
114        }
115
116        impl FromStr for $ty {
117            type Err = ValueError;
118
119            fn from_str(text: &str) -> Result<Self, Self::Err> {
120                Self::new(text)
121            }
122        }
123
124        impl TryFrom<&str> for $ty {
125            type Error = ValueError;
126
127            fn try_from(text: &str) -> Result<Self, Self::Error> {
128                Self::new(text)
129            }
130        }
131
132        impl TryFrom<String> for $ty {
133            type Error = ValueError;
134
135            fn try_from(text: String) -> Result<Self, Self::Error> {
136                Self::new(text)
137            }
138        }
139
140        impl AsRef<str> for $ty {
141            fn as_ref(&self) -> &str {
142                &self.0
143            }
144        }
145
146        impl Encode for $ty {
147            fn encode<S: Sink>(&self, w: &mut Writer<S>) {
148                w.str(&self.0);
149            }
150        }
151
152        impl Decode for $ty {
153            fn decode(r: &mut Reader<'_>, field: &'static str) -> Result<Self, DecodeError> {
154                Self::new(r.str(field)?).map_err(|error| DecodeError::Value { field, error })
155            }
156        }
157
158        #[cfg(feature = "serde")]
159        impl serde::Serialize for $ty {
160            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
161                serializer.serialize_str(&self.0)
162            }
163        }
164
165        #[cfg(feature = "serde")]
166        impl<'de> serde::Deserialize<'de> for $ty {
167            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
168                super::serde_str::deserialize(deserializer)
169            }
170        }
171    )*};
172}
173
174text_id!(LedgerId, IdempotencyKey);
175
176/// An entry's identity: 16 bytes derived from the ledger and the entry's
177/// [`IdempotencyKey`], never generated.
178///
179/// `EntryId::derive(ledger, key)` is the first 16 bytes of
180/// `SHA-256(u64 len(tag) ‖ "doubleentry/1/entry-id" ‖ str(ledger) ‖ str(key))`:
181/// deterministic, with no clock and no randomness, and committed because the
182/// key is. `Display` prints 32 lowercase hex digits; `FromStr` accepts either
183/// case.
184///
185/// ```
186/// use doubleentry::{EntryId, IdempotencyKey, LedgerId};
187///
188/// let id = EntryId::derive(&LedgerId::new("example.com/acme")?, &IdempotencyKey::new("invoice-2026-0001")?);
189/// assert_eq!(id.to_string(), "bab3f09bfedcc7d793e973e78dbe4bd4");
190/// assert_eq!("BAB3F09BFEDCC7D793E973E78DBE4BD4".parse::<EntryId>()?, id);
191/// # Ok::<(), doubleentry::error::ValueError>(())
192/// ```
193#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
194pub struct EntryId([u8; 16]);
195
196impl EntryId {
197    /// Derives the id of the entry with `key` in `ledger`.
198    #[must_use]
199    pub fn derive(ledger: &LedgerId, key: &IdempotencyKey) -> Self {
200        let mut payload = Writer::new(Vec::new());
201        payload.value(ledger);
202        payload.value(key);
203        let digest = tagged(TAG_ENTRY_ID, &payload.into_inner());
204        let mut id = [0u8; 16];
205        for (slot, byte) in id.iter_mut().zip(digest.as_bytes()) {
206            *slot = *byte;
207        }
208        Self(id)
209    }
210
211    /// An id from its 16 bytes — for decoding and references, not for new
212    /// entries.
213    #[must_use]
214    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
215        Self(bytes)
216    }
217
218    /// The 16 bytes.
219    #[must_use]
220    pub const fn as_bytes(&self) -> &[u8; 16] {
221        &self.0
222    }
223}
224
225impl fmt::Display for EntryId {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        hex::write(f, &self.0)
228    }
229}
230
231impl fmt::Debug for EntryId {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        write!(f, "EntryId({self})")
234    }
235}
236
237impl FromStr for EntryId {
238    type Err = ValueError;
239
240    fn from_str(text: &str) -> Result<Self, Self::Err> {
241        hex::parse(text).map(Self)
242    }
243}
244
245impl Encode for EntryId {
246    fn encode<S: Sink>(&self, w: &mut Writer<S>) {
247        w.fixed(&self.0);
248    }
249}
250
251impl Decode for EntryId {
252    fn decode(r: &mut Reader<'_>, _field: &'static str) -> Result<Self, DecodeError> {
253        r.array().map(Self)
254    }
255}
256
257#[cfg(feature = "serde")]
258impl serde::Serialize for EntryId {
259    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
260        serializer.collect_str(self)
261    }
262}
263
264#[cfg(feature = "serde")]
265impl<'de> serde::Deserialize<'de> for EntryId {
266    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
267        super::serde_str::deserialize(deserializer)
268    }
269}
270
271/// An account's number in its ledger: the count of registrations before it.
272///
273/// Handles index the seal balance trees and are committed in the account's
274/// register record, so one inclusion proof names the account behind a handle.
275/// The largest handle is `u32::MAX − 1`, so the number of accounts always
276/// fits a `u32`.
277///
278/// ```
279/// use doubleentry::AccountHandle;
280///
281/// assert_eq!(AccountHandle::new(7).map(AccountHandle::get), Some(7));
282/// assert_eq!(AccountHandle::new(u32::MAX), None);
283/// ```
284#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
285#[cfg_attr(feature = "serde", derive(serde::Serialize))]
286pub struct AccountHandle(u32);
287
288impl AccountHandle {
289    /// The largest handle.
290    pub const MAX: u32 = u32::MAX - 1;
291
292    /// A handle; `None` above [`MAX`](Self::MAX).
293    #[must_use]
294    pub const fn new(index: u32) -> Option<Self> {
295        if index > Self::MAX {
296            None
297        } else {
298            Some(Self(index))
299        }
300    }
301
302    /// The handle's number.
303    #[must_use]
304    pub const fn get(self) -> u32 {
305        self.0
306    }
307}
308
309impl fmt::Display for AccountHandle {
310    /// `#` and the handle's number (`#7`).
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        super::encoding::pad(f, &format!("#{}", self.0))
313    }
314}
315
316impl Encode for AccountHandle {
317    fn encode<S: Sink>(&self, w: &mut Writer<S>) {
318        w.u32(self.0);
319    }
320}
321
322impl Decode for AccountHandle {
323    fn decode(r: &mut Reader<'_>, field: &'static str) -> Result<Self, DecodeError> {
324        Self::new(r.u32()?).ok_or(DecodeError::Value {
325            field,
326            error: ValueError::OutOfRange,
327        })
328    }
329}
330
331#[cfg(feature = "serde")]
332impl<'de> serde::Deserialize<'de> for AccountHandle {
333    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
334        use serde::de::Error as _;
335        Self::new(u32::deserialize(deserializer)?)
336            .ok_or_else(|| D::Error::custom(ValueError::OutOfRange))
337    }
338}