penumbra_sdk_proof_setup/single/
log.rs

1/// The number of bytes in a contribution hash.
2pub const CONTRIBUTION_HASH_SIZE: usize = 32;
3
4/// Represents the hash of a contribution.
5///
6/// This is also used as the output of hashing CRS elements.
7#[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
36/// Represents an item that can be hashed.
37pub trait Hashable {
38    fn hash(&self) -> ContributionHash;
39}
40
41/// A trait abstracting common behavior between both setup phases.
42///
43/// This trait provides us with enough functionality to verify the contribution
44/// logs produced by both phases.
45pub trait Phase {
46    /// The type of the elements constituting this phase.
47    ///
48    /// This elements will be internally valid, although possibly not correctly linked
49    /// with respect to other contributions.
50    type CRSElements: Hashable;
51    /// The type of a contribution before any kind of validation.
52    type RawContribution: Hashable;
53    /// A contribution after having been validated.
54    type Contribution: Hashable;
55
56    /// The hash of the parent element for a raw contribution.
57    fn parent_hash(contribution: &Self::RawContribution) -> ContributionHash;
58
59    /// The elements in a contribution.
60    fn elements(contribution: &Self::Contribution) -> &Self::CRSElements;
61
62    /// Validate a contribution relative to some root elements.
63    ///
64    /// This might fail, hence the Option.
65    fn validate(
66        root: &Self::CRSElements,
67        contribution: &Self::RawContribution,
68    ) -> Option<Self::Contribution>;
69
70    /// Check if a contribution is linked to some parent elements.
71    ///
72    /// If a contribution is linked to those elements, then it builds upon those elements
73    /// correctly.
74    fn is_linked_to(contribution: &Self::Contribution, elements: &Self::CRSElements) -> bool;
75}
76
77/// A log of contributions.
78///
79/// This is just an ordered list of contributions, which should usually
80/// contain all contributions, starting from some root elements.
81pub 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    /// Scan this log for the most recent valid contribution.
93    ///
94    /// This requires the root elements which these contributions all build upon.
95    ///
96    /// This might fail to find any good contributions, in which case None will be returned.
97    pub fn last_good_contribution(
98        &self,
99        root: &P::CRSElements,
100        checkpoint: Option<ContributionHash>,
101    ) -> Option<P::Contribution> {
102        // We start by assuming that the root is correct, and the first good elements
103        // the log should start building on. From there, we keep considering new contributions,
104        // each of which can become the most recent good contribution if all conditions are met.
105        // By the end, we'll have the last good contribution for the log as a whole.
106        let root_hash = root.hash();
107        // By default, we start with 0, and no last known good contribution
108        let mut start = 0;
109        let mut last_good: Option<P::Contribution> = None;
110        // If we have a checkpoint, we might want to start at a different place, however:
111        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            // 1. Check if the parent we're building off of is the last good contribution.
127            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            // 2. Check if this contribution is internally valid.
132            let contribution = match P::validate(root, contribution) {
133                None => continue,
134                Some(c) => c,
135            };
136            // 3. Check if we're linked to the parent elements.
137            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            // 4. At this point, this is the next good contribution
145            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    // Our strategy here is to have a dummy phase setup where the validity
163    // and linkedness of elements is self-evident.
164
165    #[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    // Quick contribution creation, since we need many such examples
224    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            // Invalid
282            contribution!(1, 2, false, true),
283            // Valid
284            contribution!(1, 3, true, true),
285            // Valid, but wrong parent
286            contribution!(1, 4, true, true),
287            // Valid
288            contribution!(3, 5, true, true),
289            // Bad linking
290            contribution!(5, 6, true, false),
291            // Valid
292            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            // Invalid
305            contribution!(1, 2, false, true),
306            // Valid
307            contribution!(1, 3, true, true),
308            // Valid, but wrong parent
309            contribution!(1, 4, true, true),
310            // Valid
311            contribution!(3, 5, true, true),
312            // Bad linking
313            contribution!(5, 6, true, false),
314            // Valid
315            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            // Invalid
328            contribution!(1, 2, false, true),
329            // Valid
330            contribution!(1, 3, true, true),
331            // Valid, but wrong parent
332            contribution!(1, 4, true, true),
333            // Valid
334            contribution!(3, 5, true, true),
335            // Bad linking
336            contribution!(5, 6, true, false),
337            // Valid
338            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        // Should work because we validate internal consistency
345        assert_eq!(
346            log.last_good_contribution(&DummyElements { id: 0 }, Some(hash_from_u8(2))),
347            Some(log.contributions[6]),
348        );
349    }
350}