Skip to main content

net/adapter/net/redex/
write_token.rs

1//! `WriteToken` — typed handle to a specific write on a specific
2//! origin's chain. Returned by event-ingest paths; consumed by
3//! read-your-writes wait primitives.
4//!
5//! The substrate uses a 64-bit `origin_hash` throughout (see
6//! `identity::entity::EntityKeypair::origin_hash`). An earlier draft
7//! of the Dataforts plan speculated a 32-byte origin; the substrate
8//! shape wins because every causal-chain primitive already keys on
9//! `u64`.
10//!
11//! ## Threat model
12//!
13//! `WriteToken` is **plain in-process data**, not a signed
14//! capability. The fields are `pub` so the FFI / binding layer can
15//! marshal them without going through `serde`; in-process callers
16//! can construct any token they want. RYW guarantees are upheld by
17//! the *adapter*: `TasksAdapter::wait_for_token` and
18//! `MemoriesAdapter::wait_for_token` reject tokens whose
19//! `origin_hash` doesn't match the adapter's own bound origin
20//! ([`super::cortex::WaitForTokenError::WrongOrigin`]).
21//!
22//! This means the trust boundary is the adapter handle, not the
23//! token. A caller holding a `TasksAdapter` bound to origin X
24//! cannot use it to learn anything about origin Y by forging a
25//! token — `wait_for_token` will reject and `note_wrong_origin`
26//! will trip the metric. Tokens crossing the wire (e.g. encoded
27//! into another origin's payload) must be treated as untrusted
28//! input; the receiving side validates via the adapter binding
29//! before honoring them.
30
31use std::fmt;
32use std::str::FromStr;
33
34/// Address of a write — origin (which chain) + seq (which event on
35/// that chain). Round-trips through every binding as a typed value.
36///
37/// Treat tokens as **opaque, in-process data**. They are not
38/// signed. The fields are `pub` so FFI / binding layers can
39/// marshal them; application code should not synthesise tokens.
40/// See module-level docs for the trust model — origin-bound
41/// adapters reject mismatched tokens at `wait_for_token`.
42#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
43pub struct WriteToken {
44    /// 64-bit hash of the entity whose chain this write landed on
45    /// (`EntityKeypair::origin_hash`).
46    pub origin_hash: u64,
47    /// Per-chain monotonic sequence assigned by `RedexFile::append`.
48    pub seq: u64,
49}
50
51impl WriteToken {
52    /// Construct a token from its components. **Not for application
53    /// use** — the ingest path returns tokens with the right
54    /// `(origin_hash, seq)` already attached. Exposed only so
55    /// FFI / binding layers can marshal tokens across the
56    /// language boundary. Hand-rolled tokens that don't match an
57    /// actually-issued ingest produce waits that will hang until
58    /// the deadline expires or `WrongOrigin` rejects.
59    #[doc(hidden)]
60    pub const fn new(origin_hash: u64, seq: u64) -> Self {
61        Self { origin_hash, seq }
62    }
63}
64
65/// `<origin_hex>:<seq>` — chosen for grep-ability against the
66/// `causal:<hex>:<seq>` reserved-prefix tag shape. Stable wire
67/// form for log/CLI surfaces; bindings serialise the struct
68/// directly instead.
69impl fmt::Display for WriteToken {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{:016x}:{}", self.origin_hash, self.seq)
72    }
73}
74
75/// Errors returned by [`WriteToken::from_str`].
76#[derive(Debug, PartialEq, Eq)]
77pub enum WriteTokenParseError {
78    /// Input did not contain the `:` that separates origin from seq.
79    MissingSeparator,
80    /// Origin portion was not 16 lowercase hex characters.
81    BadOrigin,
82    /// Seq portion did not parse as a decimal `u64`.
83    BadSeq,
84}
85
86impl fmt::Display for WriteTokenParseError {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::MissingSeparator => f.write_str("expected `<origin_hex>:<seq>`"),
90            Self::BadOrigin => f.write_str("origin must be 16 lowercase hex chars"),
91            Self::BadSeq => f.write_str("seq must be a decimal u64"),
92        }
93    }
94}
95
96impl std::error::Error for WriteTokenParseError {}
97
98impl FromStr for WriteToken {
99    type Err = WriteTokenParseError;
100
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        let (origin_str, seq_str) = s
103            .split_once(':')
104            .ok_or(WriteTokenParseError::MissingSeparator)?;
105        if origin_str.len() != 16 {
106            return Err(WriteTokenParseError::BadOrigin);
107        }
108        let origin_hash =
109            u64::from_str_radix(origin_str, 16).map_err(|_| WriteTokenParseError::BadOrigin)?;
110        let seq: u64 = seq_str.parse().map_err(|_| WriteTokenParseError::BadSeq)?;
111        Ok(Self::new(origin_hash, seq))
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn display_pads_origin_to_16_hex() {
121        let token = WriteToken::new(0xDEAD_BEEF, 42);
122        assert_eq!(token.to_string(), "00000000deadbeef:42");
123    }
124
125    #[test]
126    fn display_round_trips_via_from_str() {
127        let token = WriteToken::new(0x0123_4567_89AB_CDEF, 12345);
128        let parsed: WriteToken = token.to_string().parse().unwrap();
129        assert_eq!(parsed, token);
130    }
131
132    #[test]
133    fn from_str_rejects_missing_separator() {
134        assert_eq!(
135            "deadbeef".parse::<WriteToken>(),
136            Err(WriteTokenParseError::MissingSeparator)
137        );
138    }
139
140    #[test]
141    fn from_str_rejects_short_origin() {
142        assert_eq!(
143            "deadbeef:1".parse::<WriteToken>(),
144            Err(WriteTokenParseError::BadOrigin)
145        );
146    }
147
148    #[test]
149    fn from_str_rejects_non_hex_origin() {
150        assert_eq!(
151            "zzzzzzzzzzzzzzzz:1".parse::<WriteToken>(),
152            Err(WriteTokenParseError::BadOrigin)
153        );
154    }
155
156    #[test]
157    fn from_str_rejects_bad_seq() {
158        assert_eq!(
159            "0000000000000001:-1".parse::<WriteToken>(),
160            Err(WriteTokenParseError::BadSeq)
161        );
162    }
163
164    #[test]
165    fn equality_is_componentwise() {
166        let a = WriteToken::new(1, 2);
167        let b = WriteToken::new(1, 2);
168        let c = WriteToken::new(1, 3);
169        let d = WriteToken::new(2, 2);
170        assert_eq!(a, b);
171        assert_ne!(a, c);
172        assert_ne!(a, d);
173    }
174
175    #[test]
176    fn token_is_copy() {
177        fn assert_copy<T: Copy>() {}
178        assert_copy::<WriteToken>();
179    }
180}