simplicity/node/
commit.rs

1// SPDX-License-Identifier: CC0-1.0
2
3use crate::dag::{DagLike, MaxSharing, NoSharing, PostOrderIterItem};
4use crate::jet::Jet;
5use crate::types::arrow::{Arrow, FinalArrow};
6use crate::{encode, types, Value};
7use crate::{Amr, BitIter, BitWriter, Cmr, Error, Ihr, Imr};
8
9use super::{
10    Construct, ConstructData, ConstructNode, Constructible, Converter, Inner, Marker, NoDisconnect,
11    NoWitness, Node, Redeem, RedeemNode,
12};
13
14use std::io;
15use std::marker::PhantomData;
16use std::sync::Arc;
17
18#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
19pub struct Commit<J> {
20    /// Makes the type non-constructible.
21    never: std::convert::Infallible,
22    /// Required by Rust.
23    phantom: std::marker::PhantomData<J>,
24}
25
26impl<J: Jet> Marker for Commit<J> {
27    type CachedData = Arc<CommitData<J>>;
28    type Witness = NoWitness;
29    type Disconnect = NoDisconnect;
30    type SharingId = Ihr;
31    type Jet = J;
32
33    fn compute_sharing_id(_: Cmr, cached_data: &Arc<CommitData<J>>) -> Option<Ihr> {
34        cached_data.ihr
35    }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, Hash)]
39pub struct CommitData<J> {
40    /// The source and target types of the node
41    arrow: FinalArrow,
42    /// The IMR of the node if it exists. This is distinct from its IHR; it is
43    /// used during computation of the IHR.
44    imr: Option<Imr>,
45    /// The AMR of the node if it exists, meaning, if it is not (an ancestor of)
46    /// a witness or disconnect node.
47    amr: Option<Amr>,
48    /// The IHR of the node if it exists, meaning, if it is not (an ancestor of)
49    /// a witness or disconnect node.
50    ihr: Option<Ihr>,
51    /// This isn't really necessary, but it helps type inference if every
52    /// struct has a \<J\> parameter, since it forces the choice of jets to
53    /// be consistent without the user needing to specify it too many times.
54    phantom: PhantomData<J>,
55}
56
57impl<J: Jet> CommitData<J> {
58    /// Accessor for the node's arrow
59    pub fn arrow(&self) -> &FinalArrow {
60        &self.arrow
61    }
62
63    /// Accessor for the node's IHR, if known
64    pub fn ihr(&self) -> Option<Ihr> {
65        self.ihr
66    }
67
68    /// Helper function to compute a cached AMR
69    fn incomplete_amr(
70        inner: Inner<&Arc<Self>, J, &NoDisconnect, &NoWitness>,
71        arrow: &FinalArrow,
72    ) -> Option<Amr> {
73        match inner {
74            Inner::Iden => Some(Amr::iden(arrow)),
75            Inner::Unit => Some(Amr::unit(arrow)),
76            Inner::InjL(child) => child.amr.map(|amr| Amr::injl(arrow, amr)),
77            Inner::InjR(child) => child.amr.map(|amr| Amr::injr(arrow, amr)),
78            Inner::Take(child) => child.amr.map(|amr| Amr::take(arrow, amr)),
79            Inner::Drop(child) => child.amr.map(|amr| Amr::drop(arrow, amr)),
80            Inner::Comp(left, right) => left
81                .amr
82                .zip(right.amr)
83                .map(|(a, b)| Amr::comp(arrow, &left.arrow, a, b)),
84            Inner::Case(left, right) => {
85                left.amr.zip(right.amr).map(|(a, b)| Amr::case(arrow, a, b))
86            }
87            Inner::AssertL(left, r_cmr) => left
88                .amr
89                .map(|l_amr| Amr::assertl(arrow, l_amr, r_cmr.into())),
90            Inner::AssertR(l_cmr, right) => right
91                .amr
92                .map(|r_amr| Amr::assertr(arrow, l_cmr.into(), r_amr)),
93            Inner::Pair(left, right) => left
94                .amr
95                .zip(right.amr)
96                .map(|(a, b)| Amr::pair(arrow, &left.arrow, &right.arrow, a, b)),
97            Inner::Disconnect(..) => None,
98            Inner::Witness(..) => None,
99            Inner::Fail(entropy) => Some(Amr::fail(entropy)),
100            Inner::Jet(jet) => Some(Amr::jet(jet)),
101            Inner::Word(ref val) => Some(Amr::const_word(val)),
102        }
103    }
104
105    /// Helper function to compute a cached first-pass IHR
106    fn imr(inner: Inner<&Arc<Self>, J, &NoDisconnect, &NoWitness>) -> Option<Imr> {
107        match inner {
108            Inner::Iden => Some(Imr::iden()),
109            Inner::Unit => Some(Imr::unit()),
110            Inner::InjL(child) => child.imr.map(Imr::injl),
111            Inner::InjR(child) => child.imr.map(Imr::injr),
112            Inner::Take(child) => child.imr.map(Imr::take),
113            Inner::Drop(child) => child.imr.map(Imr::drop),
114            Inner::Comp(left, right) => left.imr.zip(right.imr).map(|(a, b)| Imr::comp(a, b)),
115            Inner::Case(left, right) => left.imr.zip(right.imr).map(|(a, b)| Imr::case(a, b)),
116            Inner::AssertL(left, r_cmr) => left.imr.map(|l_ihr| Imr::case(l_ihr, r_cmr.into())),
117            Inner::AssertR(l_cmr, right) => right.imr.map(|r_ihr| Imr::case(l_cmr.into(), r_ihr)),
118            Inner::Pair(left, right) => left.imr.zip(right.imr).map(|(a, b)| Imr::pair(a, b)),
119            Inner::Disconnect(..) => None,
120            Inner::Witness(..) => None,
121            Inner::Fail(entropy) => Some(Imr::fail(entropy)),
122            Inner::Jet(jet) => Some(Imr::jet(jet)),
123            Inner::Word(ref val) => Some(Imr::const_word(val)),
124        }
125    }
126
127    pub fn new(
128        arrow: &Arrow,
129        inner: Inner<&Arc<Self>, J, &NoDisconnect, &NoWitness>,
130    ) -> Result<Self, types::Error> {
131        let final_arrow = arrow.finalize()?;
132        let imr = Self::imr(inner.clone());
133        let amr = Self::incomplete_amr(inner, &final_arrow);
134        Ok(CommitData {
135            imr,
136            amr,
137            ihr: imr.map(|ihr| Ihr::from_imr(ihr, &final_arrow)),
138            arrow: final_arrow,
139            phantom: PhantomData,
140        })
141    }
142
143    pub fn from_final(
144        arrow: FinalArrow,
145        inner: Inner<&Arc<Self>, J, &NoDisconnect, &NoWitness>,
146    ) -> Self {
147        let imr = Self::imr(inner.clone());
148        let amr = Self::incomplete_amr(inner, &arrow);
149        CommitData {
150            imr,
151            amr,
152            ihr: imr.map(|ihr| Ihr::from_imr(ihr, &arrow)),
153            arrow,
154            phantom: PhantomData,
155        }
156    }
157}
158
159pub type CommitNode<J> = Node<Commit<J>>;
160
161impl<J: Jet> CommitNode<J> {
162    /// Accessor for the node's arrow
163    pub fn arrow(&self) -> &FinalArrow {
164        &self.data.arrow
165    }
166
167    /// Accessor for the node's AMR, if known
168    pub fn amr(&self) -> Option<Amr> {
169        self.data.amr
170    }
171
172    /// Accessor for the node's IHR, if known
173    pub fn ihr(&self) -> Option<Ihr> {
174        self.data.ihr
175    }
176
177    /// Finalizes a DAG, by iterating through through it without sharing, attaching
178    /// witnesses, and hiding branches.
179    ///
180    /// This is a thin wrapper around [`Node::convert`] which fixes a few types to make
181    /// it easier to use.
182    pub fn finalize<C: Converter<Commit<J>, Redeem<J>>>(
183        &self,
184        converter: &mut C,
185    ) -> Result<Arc<RedeemNode<J>>, C::Error> {
186        self.convert::<NoSharing, Redeem<J>, _>(converter)
187    }
188
189    /// Convert a [`CommitNode`] back to a [`ConstructNode`] by redoing type inference
190    pub fn unfinalize_types(&self) -> Result<Arc<ConstructNode<J>>, types::Error> {
191        struct UnfinalizeTypes<J: Jet> {
192            inference_context: types::Context,
193            phantom: PhantomData<J>,
194        }
195
196        impl<J: Jet> Converter<Commit<J>, Construct<J>> for UnfinalizeTypes<J> {
197            type Error = types::Error;
198            fn convert_witness(
199                &mut self,
200                _: &PostOrderIterItem<&CommitNode<J>>,
201                _: &NoWitness,
202            ) -> Result<Option<Value>, Self::Error> {
203                Ok(None)
204            }
205
206            fn convert_disconnect(
207                &mut self,
208                _: &PostOrderIterItem<&CommitNode<J>>,
209                _: Option<&Arc<ConstructNode<J>>>,
210                _: &NoDisconnect,
211            ) -> Result<Option<Arc<ConstructNode<J>>>, Self::Error> {
212                Ok(None)
213            }
214
215            fn convert_data(
216                &mut self,
217                _: &PostOrderIterItem<&CommitNode<J>>,
218                inner: Inner<
219                    &Arc<ConstructNode<J>>,
220                    J,
221                    &Option<Arc<ConstructNode<J>>>,
222                    &Option<Value>,
223                >,
224            ) -> Result<ConstructData<J>, Self::Error> {
225                let inner = inner
226                    .map(|node| node.arrow())
227                    .map_disconnect(|maybe_node| maybe_node.as_ref().map(|node| node.arrow()));
228                let inner = inner.disconnect_as_ref(); // lol sigh rust
229                Ok(ConstructData::new(Arrow::from_inner(
230                    &self.inference_context,
231                    inner,
232                )?))
233            }
234        }
235
236        self.convert::<MaxSharing<Commit<J>>, _, _>(&mut UnfinalizeTypes {
237            inference_context: types::Context::new(),
238            phantom: PhantomData,
239        })
240    }
241
242    /// Decode a Simplicity program from bits, without witness data.
243    ///
244    /// # Usage
245    ///
246    /// Use this method only if the serialization **does not** include the witness data.
247    /// This means, the program simply has no witness during commitment,
248    /// or the witness is provided by other means.
249    ///
250    /// If the serialization contains the witness data, then use [`RedeemNode::decode()`].
251    pub fn decode<I: Iterator<Item = u8>>(mut bits: BitIter<I>) -> Result<Arc<Self>, Error> {
252        // 1. Decode program with out witnesses.
253        let construct = crate::decode::decode_expression(&mut bits)?;
254        bits.close()
255            .map_err(crate::decode::Error::BitIter)
256            .map_err(Error::Decode)?;
257        let program = construct.finalize_types()?;
258        // 2. Do sharing check, using incomplete IHRs
259        if program.as_ref().is_shared_as::<MaxSharing<Commit<J>>>() {
260            Ok(program)
261        } else {
262            Err(Error::Decode(crate::decode::Error::SharingNotMaximal))
263        }
264    }
265
266    /// Encode a Simplicity expression to bits without any witness data
267    pub fn encode<W: io::Write>(&self, w: &mut BitWriter<W>) -> io::Result<usize> {
268        let program_bits = encode::encode_program(self, w)?;
269        w.flush_all()?;
270        Ok(program_bits)
271    }
272
273    /// Encode a Simplicity program to a vector of bytes, without any witness data.
274    pub fn encode_to_vec(&self) -> Vec<u8> {
275        let mut program = Vec::<u8>::new();
276        let mut writer = BitWriter::new(&mut program);
277        self.encode(&mut writer)
278            .expect("write to vector never fails");
279        debug_assert!(!program.is_empty());
280
281        program
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    use hex::DisplayHex;
290    use std::fmt;
291
292    use crate::decode::Error;
293    use crate::human_encoding::Forest;
294    use crate::jet::Core;
295    use crate::node::SimpleFinalizer;
296    use crate::{BitMachine, Value};
297
298    fn assert_program_deserializable<J: Jet>(
299        prog_str: &str,
300        prog_bytes: &[u8],
301        cmr_str: &str,
302    ) -> Arc<CommitNode<J>> {
303        let forest = match Forest::<J>::parse(prog_str) {
304            Ok(forest) => forest,
305            Err(e) => panic!("Failed to parse program `{}`: {}", prog_str, e),
306        };
307        assert_eq!(
308            forest.roots().len(),
309            1,
310            "program `{}` has multiple roots",
311            prog_str
312        );
313        let main = match forest.roots().get("main") {
314            Some(root) => root,
315            None => panic!("Program `{}` has no main", prog_str),
316        };
317
318        let prog_hex = prog_bytes.as_hex();
319        let main_bytes = main.encode_to_vec();
320        assert_eq!(
321            prog_bytes,
322            main_bytes,
323            "Program string `{}` encoded to {} (expected {})",
324            prog_str,
325            main_bytes.as_hex(),
326            prog_hex,
327        );
328
329        let iter = BitIter::from(prog_bytes);
330        let prog = match CommitNode::<J>::decode(iter) {
331            Ok(prog) => prog,
332            Err(e) => panic!("program {} failed: {}", prog_hex, e),
333        };
334
335        assert_eq!(
336            prog.cmr().to_string(),
337            cmr_str,
338            "CMR mismatch (got {} expected {}) for program {}",
339            prog.cmr(),
340            cmr_str,
341            prog_hex,
342        );
343
344        let reser_sink = prog.encode_to_vec();
345        assert_eq!(
346            prog_bytes,
347            &reser_sink[..],
348            "program {} reserialized as {}",
349            prog_hex,
350            reser_sink.as_hex(),
351        );
352
353        prog
354    }
355
356    fn assert_program_not_deserializable<J: Jet>(prog: &[u8], err: &dyn fmt::Display) {
357        let prog_hex = prog.as_hex();
358        let err_str = err.to_string();
359
360        let iter = BitIter::from(prog);
361        match CommitNode::<J>::decode(iter) {
362            Ok(prog) => panic!(
363                "Program {} succeded (expected error {}). Program parsed as:\n{}",
364                prog_hex, err, prog
365            ),
366            Err(e) if e.to_string() == err_str => {} // ok
367            Err(e) => panic!(
368                "Program {} failed with error {} (expected error {})",
369                prog_hex, e, err
370            ),
371        };
372    }
373
374    #[test]
375    fn canonical_order() {
376        // "main = comp unit iden", but with the iden serialized before the unit
377        // To obtain this test vector I temporarily swapped `get_left` and `get_right`
378        // in the implementation of `PostOrderIter`
379        assert_program_not_deserializable::<Core>(&[0xa8, 0x48, 0x10], &Error::NotInCanonicalOrder);
380
381        // "main = iden", but prefixed by some unused nodes, the first of which is also iden.
382        assert_program_not_deserializable::<Core>(
383            &[0xc1, 0x00, 0x06, 0x20],
384            &Error::NotInCanonicalOrder,
385        );
386    }
387
388    #[test]
389    fn hidden_node() {
390        // main = hidden deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
391        #[rustfmt::skip]
392        let hidden = [
393            0x36, 0xf5, 0x6d, 0xf7, 0x7e, 0xf5, 0x6d, 0xf7,
394            0x7e, 0xf5, 0x6d, 0xf7, 0x7e, 0xf5, 0x6d, 0xf7,
395            0x7e, 0xf5, 0x6d, 0xf7, 0x7e, 0xf5, 0x6d, 0xf7,
396            0x7e, 0xf5, 0x6d, 0xf7, 0x7e, 0xf5, 0x6d, 0xf7,
397            78,
398        ];
399        assert_program_not_deserializable::<Core>(&hidden, &Error::HiddenNode);
400
401        // main = comp witness hidden deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
402        let hidden = [
403            0xae, 0xdb, 0xd5, 0xb7, 0xdd, 0xfb, 0xd5, 0xb7, 0xdd, 0xfb, 0xd5, 0xb7, 0xdd, 0xfb,
404            0xd5, 0xb7, 0xdd, 0xfb, 0xd5, 0xb7, 0xdd, 0xfb, 0xd5, 0xb7, 0xdd, 0xfb, 0xd5, 0xb7,
405            0xdd, 0xfb, 0xd5, 0xb7, 0xdd, 0xe0, 0x80,
406        ];
407        assert_program_not_deserializable::<Core>(&hidden, &Error::HiddenNode);
408    }
409
410    #[test]
411    fn case_both_children_hidden() {
412        // h1 = hidden deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
413        // main = case h1 h1
414        #[rustfmt::skip]
415        let hidden = [
416            0x8d, 0xbd, 0x5b, 0x7d, 0xdf, 0xbd, 0x5b, 0x7d,
417            0xdf, 0xbd, 0x5b, 0x7d, 0xdf, 0xbd, 0x5b, 0x7d,
418            0xdf, 0xbd, 0x5b, 0x7d, 0xdf, 0xbd, 0x5b, 0x7d,
419            0xdf, 0xbd, 0x5b, 0x7d, 0xdf, 0xbd, 0x5b, 0x7d,
420            0xde, 0x10,
421        ];
422        assert_program_not_deserializable::<Core>(&hidden, &Error::BothChildrenHidden);
423    }
424
425    #[test]
426    fn unshared_hidden() {
427        // This program has a repeated hidden node, but all other sharing is correct
428        // and the order is canonical, etc.
429        #[rustfmt::skip]
430        let hidden = [
431            0xd6, 0xe9, 0x62, 0x56, 0x62, 0xc9, 0x38, 0x8a,
432            0x44, 0x31, 0x85, 0xee, 0xc2, 0x2b, 0x91, 0x48,
433            0x87, 0xe1, 0xfd, 0x18, 0x57, 0xc2, 0x8c, 0x4a,
434            0x28, 0x44, 0x2f, 0xa8, 0x61, 0x5c, 0xa7, 0x6e,
435            0x8c, 0xf9, 0x80, 0xc2, 0x18, 0x95, 0x98, 0xb2,
436            0x4e, 0x22, 0x91, 0x0c, 0x61, 0x7b, 0xb0, 0x8a,
437            0xe4, 0x52, 0x21, 0xf8, 0x7f, 0x46, 0x15, 0xf0,
438            0xa3, 0x12, 0x8a, 0x11, 0x0b, 0xea, 0x18, 0x57,
439            0x29, 0xdb, 0xa3, 0x3e, 0x60, 0x30, 0x2c, 0x00,
440            0xd0, 0x48, 0x20,
441        ];
442        assert_program_not_deserializable::<Core>(&hidden, &Error::SharingNotMaximal);
443    }
444
445    #[test]
446    fn shared_witnesses() {
447        assert_program_deserializable::<Core>(
448            "main := witness",
449            &[0x38],
450            "a0fc8debd6796917c86b77aded82e6c61649889ae8f2ed65b57b41aa9d90e375",
451        );
452
453        #[rustfmt::skip]
454        let bad_diff1s = vec![
455            // Above program, but with both witness nodes shared (note they have
456            // the same type and CMR)
457            vec![
458                0xda, 0xe2, 0x39, 0xa3, 0x10, 0x42, 0x0e, 0x05,
459                0x71, 0x88, 0xa3, 0x6d, 0xc4, 0x11, 0x80, 0x80
460            ],
461            // Same program but with each `witness` replaced by `comp iden witness`, which
462            // is semantically the same but buries the offending witness nodes a bit to
463            // trip up naive sharing logic.
464            vec![
465                0xde, 0x87, 0x04, 0x08, 0xe6, 0x8c, 0x41, 0x08,
466                0x38, 0x15, 0xc6, 0x22, 0x8d, 0xb7, 0x10, 0x46,
467                0x02, 0x00,
468            ],
469        ];
470        for bad_diff1 in bad_diff1s {
471            assert_program_not_deserializable::<Core>(&bad_diff1, &Error::SharingNotMaximal);
472        }
473
474        #[rustfmt::skip]
475        let diff1s = vec![
476            (
477                // Sharing corrected
478                "
479                    -- Program which demands two 32-bit witnesses, the first one == the second + 1
480                    wit1 := witness : 1 -> 2^32
481                    wit2 := witness : 1 -> 2^32
482
483                    wit_diff := comp (comp (pair wit1 wit2) jet_subtract_32) (drop iden) : 1 -> 2^32
484                    diff_is_one := comp (pair wit_diff jet_one_32) jet_eq_32             : 1 -> 2
485                    main := comp diff_is_one jet_verify                                  : 1 -> 1
486                ",
487                vec![
488                    0xdc, 0xee, 0x28, 0xe6, 0x8c, 0x41, 0x08, 0x38,
489                    0x15, 0xc6, 0x22, 0x8d, 0xb7, 0x10, 0x46, 0x02,
490                    0x00,
491                ],
492                // CMR not checked against C code, since C won't give us any data without witnesses
493                "e9339a0d715c721bff752aedc02710cdf3399f3f8d86e64456e85a1bc06ecb7c",
494            ),
495            // Same program but with each `witness` replaced by `comp iden witness`.
496            (
497                "
498                    -- Program which demands two 32-bit witnesses, the first one == the second + 1
499                    wit1 := witness : 1 -> 2^32
500                    wit2 := witness : 1 -> 2^32
501                    compwit1 := comp iden wit1
502                    compwit2 := comp iden wit2
503
504                    wit_diff := comp (comp (pair compwit1 compwit2) jet_subtract_32) (drop iden)
505                    diff_is_one := comp (pair wit_diff jet_one_32) jet_eq_32             : 1 -> 2
506                    main := comp diff_is_one jet_verify                                  : 1 -> 1
507                ",
508                vec![
509                    0xe0, 0x28, 0x70, 0x43, 0x83, 0x00, 0xab, 0x9a,
510                    0x31, 0x04, 0x20, 0xe0, 0x57, 0x18, 0x8a, 0x36,
511                    0xdc, 0x41, 0x18, 0x08,
512                ],
513                // CMR not checked against C code, since C won't give us any data without witnesses
514                "d03bf350f406aef3af0d48e6533b3325ff86f18a36e0e73895a5cd6d6692b860",
515            )
516        ];
517
518        for (prog_str, diff1, cmr) in diff1s {
519            let diff1_prog = crate::node::commit::tests::assert_program_deserializable::<Core>(
520                prog_str, &diff1, cmr,
521            );
522
523            // Attempt to finalize, providing 32-bit witnesses 0, 1, ..., and then
524            // counting how many were consumed afterward.
525            let mut counter = 0..100;
526            let witness_iter = (&mut counter).rev().map(Value::u32);
527            let diff1_final = diff1_prog
528                .finalize(&mut SimpleFinalizer::new(witness_iter))
529                .unwrap();
530            assert_eq!(counter, 0..98);
531
532            // Execute the program to confirm that it worked
533            let mut mac =
534                BitMachine::for_program(&diff1_final).expect("program has reasonable bounds");
535            mac.exec(&diff1_final, &()).unwrap();
536        }
537    }
538
539    #[test]
540    fn extra_nodes() {
541        // main = comp unit unit # but with an extra unconnected `unit` stuck on the beginning
542        // I created this unit test by hand
543        assert_program_not_deserializable::<Core>(&[0xa9, 0x48, 0x00], &Error::NotInCanonicalOrder);
544    }
545
546    #[test]
547    fn regression_177() {
548        // `case (drop iden) iden` from upstream occurs-check test. Has an infinitely sized
549        // input type. Will fail trying to unify the input type with the unit type before
550        // doing the occurs check, putting an infinitely-sized type into the error variant.
551        //
552        // The human-readable encoding will keep going and then also hit the occurs check.
553        //
554        // Check that both error types can be generated and printed in finite space/time.
555        let bad_prog = "
556            id := iden
557            main := case (drop id) id
558        ";
559        match Forest::<Core>::parse(bad_prog) {
560            Ok(_) => panic!("program should have failed"),
561            Err(set) => {
562                let mut errs_happened = (false, false);
563                for err in set.iter() {
564                    match err {
565                        crate::human_encoding::Error::TypeCheck(e @ types::Error::Bind { .. }) => {
566                            errs_happened.0 = true;
567                            e.to_string();
568                        }
569                        crate::human_encoding::Error::TypeCheck(
570                            e @ types::Error::OccursCheck { .. },
571                        ) => {
572                            errs_happened.1 = true;
573                            e.to_string();
574                        }
575                        x => panic!("unexpected error {x:?}"),
576                    }
577                }
578                assert_eq!(errs_happened, (true, true));
579            }
580        };
581    }
582}