1pub const CONTRIBUTION_HASH_SIZE: usize = 32;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub struct ContributionHash(pub [u8; CONTRIBUTION_HASH_SIZE]);
9
10impl ContributionHash {
11 pub(crate) fn dummy() -> Self {
12 Self([0x1; CONTRIBUTION_HASH_SIZE])
13 }
14}
15
16impl AsRef<[u8]> for ContributionHash {
17 fn as_ref(&self) -> &[u8] {
18 &self.0
19 }
20}
21
22impl TryFrom<&[u8]> for ContributionHash {
23 type Error = anyhow::Error;
24
25 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
26 if value.len() != CONTRIBUTION_HASH_SIZE {
27 anyhow::bail!(
28 "Failed to read ContributionHash from slice of len {}",
29 value.len()
30 );
31 }
32 Ok(Self(value.try_into()?))
33 }
34}
35
36pub trait Hashable {
38 fn hash(&self) -> ContributionHash;
39}
40
41pub trait Phase {
46 type CRSElements: Hashable;
51 type RawContribution: Hashable;
53 type Contribution: Hashable;
55
56 fn parent_hash(contribution: &Self::RawContribution) -> ContributionHash;
58
59 fn elements(contribution: &Self::Contribution) -> &Self::CRSElements;
61
62 fn validate(
66 root: &Self::CRSElements,
67 contribution: &Self::RawContribution,
68 ) -> Option<Self::Contribution>;
69
70 fn is_linked_to(contribution: &Self::Contribution, elements: &Self::CRSElements) -> bool;
75}
76
77pub struct ContributionLog<P: Phase> {
82 contributions: Vec<P::RawContribution>,
83}
84
85impl<P: Phase> ContributionLog<P> {
86 pub fn new(contributions: impl IntoIterator<Item = P::RawContribution>) -> Self {
87 Self {
88 contributions: contributions.into_iter().collect(),
89 }
90 }
91
92 pub fn last_good_contribution(
98 &self,
99 root: &P::CRSElements,
100 checkpoint: Option<ContributionHash>,
101 ) -> Option<P::Contribution> {
102 let root_hash = root.hash();
107 let mut start = 0;
109 let mut last_good: Option<P::Contribution> = None;
110 if let Some(checkpoint) = checkpoint {
112 if let Some((i, matching)) = self
113 .contributions
114 .iter()
115 .enumerate()
116 .find(|(_, c)| c.hash() == checkpoint)
117 {
118 if let Some(valid) = P::validate(root, matching) {
119 start = i + 1;
120 last_good = Some(valid);
121 }
122 }
123 }
124
125 for contribution in &self.contributions[start..] {
126 let expected_parent_hash = last_good.as_ref().map(|c| c.hash()).unwrap_or(root_hash);
128 if P::parent_hash(contribution) != expected_parent_hash {
129 continue;
130 }
131 let contribution = match P::validate(root, contribution) {
133 None => continue,
134 Some(c) => c,
135 };
136 let linked = match last_good.as_ref() {
138 None => P::is_linked_to(&contribution, root),
139 Some(c) => P::is_linked_to(&contribution, P::elements(c)),
140 };
141 if !linked {
142 continue;
143 }
144 last_good = Some(contribution)
146 }
147
148 last_good
149 }
150}
151
152#[cfg(test)]
153mod test {
154 use super::*;
155
156 fn hash_from_u8(x: u8) -> ContributionHash {
157 let mut out = [0u8; CONTRIBUTION_HASH_SIZE];
158 out[0] = x;
159 ContributionHash(out)
160 }
161
162 #[derive(Clone, Copy, Debug, PartialEq)]
166 struct DummyElements {
167 id: u8,
168 }
169
170 impl Hashable for DummyElements {
171 fn hash(&self) -> ContributionHash {
172 hash_from_u8(self.id)
173 }
174 }
175
176 #[derive(Clone, Copy, Debug, PartialEq)]
177 struct DummyContribution {
178 parent: u8,
179 elements: DummyElements,
180 valid: bool,
181 linked: bool,
182 }
183
184 impl Hashable for DummyContribution {
185 fn hash(&self) -> ContributionHash {
186 self.elements.hash()
187 }
188 }
189
190 struct DummyPhase;
191
192 impl Phase for DummyPhase {
193 type CRSElements = DummyElements;
194
195 type RawContribution = DummyContribution;
196
197 type Contribution = DummyContribution;
198
199 fn parent_hash(contribution: &Self::RawContribution) -> ContributionHash {
200 hash_from_u8(contribution.parent)
201 }
202
203 fn elements(contribution: &Self::Contribution) -> &Self::CRSElements {
204 &contribution.elements
205 }
206
207 fn validate(
208 _root: &Self::CRSElements,
209 contribution: &Self::RawContribution,
210 ) -> Option<Self::Contribution> {
211 if contribution.valid {
212 Some(*contribution)
213 } else {
214 None
215 }
216 }
217
218 fn is_linked_to(contribution: &Self::Contribution, _elements: &Self::CRSElements) -> bool {
219 contribution.linked
220 }
221 }
222
223 macro_rules! contribution {
225 ($parent: expr, $id: expr, $valid: expr, $linked: expr) => {
226 DummyContribution {
227 parent: $parent,
228 elements: DummyElements { id: $id },
229 valid: $valid,
230 linked: $linked,
231 }
232 };
233 }
234
235 #[test]
236 fn test_empty_log_search_returns_none() {
237 let log: ContributionLog<DummyPhase> = ContributionLog::new(vec![]);
238 assert!(log
239 .last_good_contribution(&DummyElements { id: 0 }, None)
240 .is_none());
241 }
242
243 #[test]
244 fn test_single_valid_log_search() {
245 let log: ContributionLog<DummyPhase> =
246 ContributionLog::new(vec![contribution!(0, 1, true, true)]);
247 assert_eq!(
248 log.last_good_contribution(&DummyElements { id: 0 }, None),
249 Some(log.contributions[0])
250 );
251 }
252
253 #[test]
254 fn test_multi_valid_log_search() {
255 let log: ContributionLog<DummyPhase> = ContributionLog::new(vec![
256 contribution!(0, 1, true, true),
257 contribution!(1, 2, true, true),
258 ]);
259 assert_eq!(
260 log.last_good_contribution(&DummyElements { id: 0 }, None),
261 Some(log.contributions[1])
262 );
263 }
264
265 #[test]
266 fn test_multi_valid_log_search_bad_root() {
267 let log: ContributionLog<DummyPhase> = ContributionLog::new(vec![
268 contribution!(0, 1, true, true),
269 contribution!(1, 2, true, true),
270 ]);
271 assert_eq!(
272 log.last_good_contribution(&DummyElements { id: 2 }, None),
273 None
274 );
275 }
276
277 #[test]
278 fn test_multi_log_with_skips_search() {
279 let log: ContributionLog<DummyPhase> = ContributionLog::new(vec![
280 contribution!(0, 1, true, true),
281 contribution!(1, 2, false, true),
283 contribution!(1, 3, true, true),
285 contribution!(1, 4, true, true),
287 contribution!(3, 5, true, true),
289 contribution!(5, 6, true, false),
291 contribution!(5, 7, true, true),
293 ]);
294 assert_eq!(
295 log.last_good_contribution(&DummyElements { id: 0 }, None),
296 Some(log.contributions[6])
297 );
298 }
299
300 #[test]
301 fn test_multi_log_with_skips_and_resuming_search() {
302 let log: ContributionLog<DummyPhase> = ContributionLog::new(vec![
303 contribution!(0, 1, true, true),
304 contribution!(1, 2, false, true),
306 contribution!(1, 3, true, true),
308 contribution!(1, 4, true, true),
310 contribution!(3, 5, true, true),
312 contribution!(5, 6, true, false),
314 contribution!(5, 7, true, true),
316 ]);
317 assert_eq!(
318 log.last_good_contribution(&DummyElements { id: 0 }, Some(hash_from_u8(5))),
319 Some(log.contributions[6]),
320 );
321 }
322
323 #[test]
324 fn test_multi_log_with_skips_and_bad_checkpoint() {
325 let log: ContributionLog<DummyPhase> = ContributionLog::new(vec![
326 contribution!(0, 1, true, true),
327 contribution!(1, 2, false, true),
329 contribution!(1, 3, true, true),
331 contribution!(1, 4, true, true),
333 contribution!(3, 5, true, true),
335 contribution!(5, 6, true, false),
337 contribution!(5, 7, true, true),
339 ]);
340 assert_eq!(
341 log.last_good_contribution(&DummyElements { id: 0 }, Some(hash_from_u8(100))),
342 Some(log.contributions[6]),
343 );
344 assert_eq!(
346 log.last_good_contribution(&DummyElements { id: 0 }, Some(hash_from_u8(2))),
347 Some(log.contributions[6]),
348 );
349 }
350}