Skip to main content

hop_channels/
lib.rs

1#![forbid(unsafe_code)]
2//! Persistent state machines driven by HOP polynomial actions.
3
4use std::io;
5
6use hop_actions::{apply_action, Action, ActionError};
7use hop_core::{CompanionMatrix, CoreError, Field, StreamGenerator};
8
9#[cfg(feature = "torsor_masking")]
10pub mod mask;
11pub mod proj;
12pub mod store;
13pub mod store_fs;
14
15use crate::proj::try_row0_of_poly;
16use store::{ChannelSnapshot, ChannelStore};
17
18/// Errors returned by checked channel operations.
19#[derive(thiserror::Error, Debug)]
20#[non_exhaustive]
21pub enum ChannelError {
22    /// The initial or projected vector has the wrong dimension.
23    #[error("dimension mismatch: expected {expected}, got {actual}")]
24    DimensionMismatch { expected: usize, actual: usize },
25    /// An action violated the channel action contract.
26    #[error("invalid action: {0}")]
27    Action(#[from] ActionError),
28    /// HOP Core rejected a checked state or transform operation.
29    #[error("invalid channel algebra: {0}")]
30    Core(#[from] CoreError),
31    /// Persistent storage failed.
32    #[error("channel storage failed: {0}")]
33    Storage(#[from] io::Error),
34    /// No snapshot existed for the requested channel.
35    #[error("channel snapshot was not found")]
36    SnapshotNotFound,
37    /// The loaded snapshot belonged to a different channel identifier.
38    #[error("snapshot channel identifier does not match requested identifier")]
39    SnapshotIdMismatch,
40    /// Applying another action would overflow the channel height.
41    #[error("channel height exhausted")]
42    HeightOverflow,
43}
44
45/// A persistent channel state machine.
46pub struct Channel {
47    id: [u8; 32],
48    c: CompanionMatrix,
49    state: Vec<Field>,
50    height: u64,
51    store: Option<Box<dyn ChannelStore>>,
52}
53
54/// Marker representing the producer side of a channel.
55pub struct Producer;
56
57/// A checked linear projection over channel states.
58pub struct Subscriber {
59    row0: Vec<Field>,
60}
61
62impl Channel {
63    /// Opens a channel after checking that its initial state has the right size.
64    pub fn try_open(
65        c: CompanionMatrix,
66        init: Vec<Field>,
67        id: [u8; 32],
68    ) -> Result<Self, ChannelError> {
69        let expected = c.dimension();
70        if init.len() != expected {
71            return Err(ChannelError::DimensionMismatch {
72                expected,
73                actual: init.len(),
74            });
75        }
76        Ok(Self {
77            id,
78            c,
79            state: init,
80            height: 0,
81            store: None,
82        })
83    }
84
85    /// Returns the channel identifier.
86    pub const fn id(&self) -> &[u8; 32] {
87        &self.id
88    }
89
90    /// Returns the immutable companion matrix defining this channel.
91    pub const fn companion(&self) -> &CompanionMatrix {
92        &self.c
93    }
94
95    /// Returns the current channel state.
96    pub fn state(&self) -> &[Field] {
97        &self.state
98    }
99
100    /// Returns the channel dimension.
101    pub fn dimension(&self) -> usize {
102        self.c.dimension()
103    }
104
105    /// Returns the number of committed actions.
106    pub const fn height(&self) -> u64 {
107        self.height
108    }
109
110    /// Returns whether this channel has a persistence backend attached.
111    pub const fn has_store(&self) -> bool {
112        self.store.is_some()
113    }
114
115    /// Attaches a persistence backend.
116    pub fn with_store(mut self, store: Box<dyn ChannelStore>) -> Self {
117        self.store = Some(store);
118        self
119    }
120
121    /// Builds a complete channel snapshot.
122    pub fn snapshot(&self) -> ChannelSnapshot {
123        self.snapshot_with(&self.state, self.height)
124    }
125
126    /// Returns the producer-side marker for this channel.
127    pub const fn producer(&self) -> Producer {
128        Producer
129    }
130
131    /// Builds a checked subscriber projection from polynomial coefficients.
132    pub fn try_subscribe_with_g(&self, g_coeffs: &[u64]) -> Result<Subscriber, ChannelError> {
133        let action = Action {
134            g_coeffs: g_coeffs.to_vec(),
135            nonce: 0,
136            mask_row0: None,
137        };
138        action.validate_for_dimension(self.dimension())?;
139        Ok(Subscriber {
140            row0: try_row0_of_poly(&self.c, g_coeffs)?,
141        })
142    }
143
144    /// Applies and durably records one polynomial action.
145    ///
146    /// The action's optional mask is metadata for projection protocols; channel
147    /// state evolution is defined solely by `state <- g(C) * state`.
148    pub fn apply(&mut self, action: &Action) -> Result<(), ChannelError> {
149        let next_state = apply_action(&self.c, &self.state, action)?;
150        let next_height = self
151            .height
152            .checked_add(1)
153            .ok_or(ChannelError::HeightOverflow)?;
154
155        if let Some(store) = &self.store {
156            store.append_action(&self.id, next_height, action)?;
157        }
158
159        self.state = next_state;
160        self.height = next_height;
161
162        Ok(())
163    }
164
165    /// Saves a checkpoint when a storage backend is attached.
166    pub fn save_now(&self) -> Result<(), ChannelError> {
167        if let Some(store) = &self.store {
168            store.save(&self.snapshot())?;
169        }
170        Ok(())
171    }
172
173    /// Restores a checkpoint and replays actions after its height.
174    pub fn restore(id: [u8; 32], store: Box<dyn ChannelStore>) -> Result<Self, ChannelError> {
175        let snapshot = store.load(&id)?.ok_or(ChannelError::SnapshotNotFound)?;
176        if snapshot.id != id {
177            return Err(ChannelError::SnapshotIdMismatch);
178        }
179        let mut channel = Self::try_open(snapshot.c, snapshot.state, id)?;
180        channel.height = snapshot.height;
181
182        let actions = store.read_actions_from(&id, channel.height)?;
183        for action in actions {
184            channel.apply_without_persistence(&action)?;
185        }
186        channel.store = Some(store);
187        Ok(channel)
188    }
189
190    fn snapshot_with(&self, state: &[Field], height: u64) -> ChannelSnapshot {
191        ChannelSnapshot {
192            id: self.id,
193            c: self.c.clone(),
194            state: state.to_vec(),
195            height,
196        }
197    }
198
199    fn apply_without_persistence(&mut self, action: &Action) -> Result<(), ChannelError> {
200        let next_state = apply_action(&self.c, &self.state, action)?;
201        let next_height = self
202            .height
203            .checked_add(1)
204            .ok_or(ChannelError::HeightOverflow)?;
205        self.state = next_state;
206        self.height = next_height;
207        Ok(())
208    }
209}
210
211impl Subscriber {
212    /// Returns the checked projection row.
213    pub fn row(&self) -> &[Field] {
214        &self.row0
215    }
216
217    /// Projects a dimension-matched state into one field element.
218    pub fn project(&self, state: &[Field]) -> Result<Field, ChannelError> {
219        if state.len() != self.row0.len() {
220            return Err(ChannelError::DimensionMismatch {
221                expected: self.row0.len(),
222                actual: state.len(),
223            });
224        }
225        Ok(self
226            .row0
227            .iter()
228            .zip(state)
229            .fold(Field::zero(), |sum, (weight, value)| {
230                sum + (*weight * *value)
231            }))
232    }
233
234    /// Generates `n` projected stream elements.
235    pub fn stream(
236        &self,
237        mut stream: StreamGenerator,
238        n: usize,
239    ) -> Result<Vec<Field>, ChannelError> {
240        let dimension = stream.dimension();
241        if self.row0.len() != dimension {
242            return Err(ChannelError::DimensionMismatch {
243                expected: dimension,
244                actual: self.row0.len(),
245            });
246        }
247        let mut transform = vec![vec![Field::zero(); dimension]; dimension];
248        transform[0] = self.row0.clone();
249        stream.set_transform(transform)?;
250        Ok((0..n).map(|_| stream.step()).collect())
251    }
252}
253
254#[cfg(feature = "torsor_masking")]
255impl Subscriber {
256    /// Produces a twisted projection row `row * h(C)`.
257    pub fn twisted_row(
258        &self,
259        c: &CompanionMatrix,
260        nonce: u64,
261        degree: usize,
262    ) -> Result<Vec<Field>, crate::mask::MaskError> {
263        crate::mask::try_apply_twist_row(c, &self.row0, nonce, degree)
264    }
265}