penumbra_sdk_tct/
commitment.rs

1use decaf377::Fq;
2use penumbra_sdk_proto::{penumbra::crypto::tct::v1 as pb, DomainType};
3
4/// A commitment to a note or swap.
5#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
6#[serde(into = "pb::StateCommitment", try_from = "pb::StateCommitment")]
7pub struct StateCommitment(pub Fq);
8
9/// An error when decoding a commitment from a hex string.
10#[derive(Clone, Debug, thiserror::Error)]
11pub enum ParseCommitmentError {
12    /// The string was not a hex string.
13    #[error(transparent)]
14    InvalidHex(#[from] hex::FromHexError),
15    /// The bytes did not encode a valid commitment.
16    #[error(transparent)]
17    InvalidCommitment(#[from] InvalidStateCommitment),
18}
19
20impl StateCommitment {
21    /// Parse a hex string as a [`Commitment`].
22    pub fn parse_hex(str: &str) -> Result<StateCommitment, ParseCommitmentError> {
23        let bytes = hex::decode(str)?;
24        Ok(StateCommitment::try_from(&bytes[..])?)
25    }
26}
27
28impl DomainType for StateCommitment {
29    type Proto = pb::StateCommitment;
30}
31
32#[cfg(test)]
33mod test_serde {
34    use super::StateCommitment;
35
36    #[test]
37    fn roundtrip_json_zero() {
38        let commitment = StateCommitment::try_from([0; 32]).unwrap();
39        let bytes = serde_json::to_vec(&commitment).unwrap();
40        println!("{bytes:?}");
41        let deserialized: StateCommitment = serde_json::from_slice(&bytes).unwrap();
42        assert_eq!(commitment, deserialized);
43    }
44
45    /*
46    Disabled; pbjson_build derived implementations don't play well with bincode,
47    because of the issue described here: https://github.com/bincode-org/bincode/issues/276
48    #[test]
49    fn roundtrip_bincode_zero() {
50        let commitment = Commitment::try_from([0; 32]).unwrap();
51        let bytes = bincode::serialize(&commitment).unwrap();
52        println!("{:?}", bytes);
53        let deserialized: Commitment = bincode::deserialize(&bytes).unwrap();
54        assert_eq!(commitment, deserialized);
55    }
56     */
57}
58
59impl From<StateCommitment> for pb::StateCommitment {
60    fn from(nc: StateCommitment) -> Self {
61        Self {
62            inner: nc.0.to_bytes().to_vec(),
63        }
64    }
65}
66
67/// Error returned when a note commitment cannot be deserialized because it is not in range.
68#[derive(thiserror::Error, Debug, Clone, Copy)]
69#[error("Invalid note commitment")]
70pub struct InvalidStateCommitment;
71
72impl TryFrom<pb::StateCommitment> for StateCommitment {
73    type Error = InvalidStateCommitment;
74
75    fn try_from(value: pb::StateCommitment) -> Result<Self, Self::Error> {
76        let bytes: [u8; 32] = value.inner[..]
77            .try_into()
78            .map_err(|_| InvalidStateCommitment)?;
79
80        let inner = Fq::from_bytes_checked(&bytes).map_err(|_| InvalidStateCommitment)?;
81
82        Ok(StateCommitment(inner))
83    }
84}
85
86impl std::fmt::Display for StateCommitment {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.write_str(&hex::encode(&self.0.to_bytes()[..]))
89    }
90}
91
92impl std::fmt::Debug for StateCommitment {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        f.write_fmt(format_args!(
95            "note::Commitment({})",
96            hex::encode(&self.0.to_bytes()[..])
97        ))
98    }
99}
100
101impl From<StateCommitment> for [u8; 32] {
102    fn from(commitment: StateCommitment) -> [u8; 32] {
103        commitment.0.to_bytes()
104    }
105}
106
107impl TryFrom<[u8; 32]> for StateCommitment {
108    type Error = InvalidStateCommitment;
109
110    fn try_from(bytes: [u8; 32]) -> Result<StateCommitment, Self::Error> {
111        let inner = Fq::from_bytes_checked(&bytes).map_err(|_| InvalidStateCommitment)?;
112
113        Ok(StateCommitment(inner))
114    }
115}
116
117// TODO: remove? aside from sqlx is there a use case for non-proto conversion from byte slices?
118impl TryFrom<&[u8]> for StateCommitment {
119    type Error = InvalidStateCommitment;
120
121    fn try_from(slice: &[u8]) -> Result<StateCommitment, Self::Error> {
122        let bytes: [u8; 32] = slice[..].try_into().map_err(|_| InvalidStateCommitment)?;
123
124        let inner = Fq::from_bytes_checked(&bytes).map_err(|_| InvalidStateCommitment)?;
125
126        Ok(StateCommitment(inner))
127    }
128}
129
130#[cfg(feature = "arbitrary")]
131pub use arbitrary::FqStrategy;
132
133#[cfg(feature = "arbitrary")]
134mod arbitrary {
135    use decaf377::Fq;
136    use proptest::strategy::Strategy;
137
138    use super::StateCommitment;
139
140    // Arbitrary implementation for [`Commitment`]s.
141    impl proptest::arbitrary::Arbitrary for StateCommitment {
142        type Parameters = Vec<StateCommitment>;
143
144        fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
145            FqStrategy(args.into_iter().map(|commitment| commitment.0).collect())
146                .prop_map(StateCommitment)
147        }
148
149        type Strategy = proptest::strategy::Map<FqStrategy, fn(Fq) -> StateCommitment>;
150    }
151
152    /// A [`proptest`] [`Strategy`](proptest::strategy::Strategy) for generating [`Fq`]s.
153    #[derive(Clone, Debug, PartialEq, Eq, Default)]
154    pub struct FqStrategy(Vec<Fq>);
155
156    impl FqStrategy {
157        /// Create a new [`FqStrategy`] that will generate arbitrary [`Commitment`]s.
158        pub fn arbitrary() -> Self {
159            Self::one_of(vec![])
160        }
161
162        /// Create a new [`FqStrategy`] that will only produce the given [`Fq`]s.
163        ///
164        /// If the given vector is empty, this will generate arbitrary [`Fq`]s instead.
165        pub fn one_of(commitments: Vec<Fq>) -> Self {
166            FqStrategy(commitments)
167        }
168    }
169
170    impl proptest::strategy::Strategy for FqStrategy {
171        type Tree = proptest::strategy::Filter<proptest::strategy::Just<Fq>, fn(&Fq) -> bool>;
172
173        type Value = Fq;
174
175        fn new_tree(
176            &self,
177            runner: &mut proptest::test_runner::TestRunner,
178        ) -> proptest::strategy::NewTree<Self> {
179            use proptest::prelude::{Rng, RngCore};
180            let rng = runner.rng();
181            Ok(if !self.0.is_empty() {
182                proptest::strategy::Just(
183                    *rng.sample(rand::distributions::Slice::new(&self.0).expect("empty vector")),
184                )
185            } else {
186                let mut bytes = [0u8; 32];
187                rng.fill_bytes(&mut bytes);
188                proptest::strategy::Just(decaf377::Fq::from_le_bytes_mod_order(&bytes))
189            }
190            .prop_filter("impossible", |_| true))
191        }
192    }
193}