masp_primitives/
convert.rs

1use crate::{
2    sapling::{
3        pedersen_hash::{pedersen_hash, Personalization},
4        Node, ValueCommitment,
5    },
6    transaction::components::amount::{I128Sum, ValueSum},
7};
8use borsh::schema::add_definition;
9use borsh::schema::Declaration;
10use borsh::schema::Definition;
11use borsh::schema::Fields;
12use borsh::BorshSchema;
13use borsh::{BorshDeserialize, BorshSerialize};
14use group::{Curve, GroupEncoding};
15use std::collections::BTreeMap;
16use std::{
17    io::{self, Write},
18    iter::Sum,
19    ops::{Add, AddAssign, Neg, Sub, SubAssign},
20};
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct AllowedConversion {
24    /// The asset type that the note represents
25    assets: I128Sum,
26    /// Memorize generator because it's expensive to recompute
27    generator: jubjub::ExtendedPoint,
28}
29
30impl AllowedConversion {
31    pub fn uncommitted() -> bls12_381::Scalar {
32        // The smallest u-coordinate that is not on the curve
33        // is one.
34        bls12_381::Scalar::one()
35    }
36
37    /// Computes the note commitment, returning the full point.
38    fn cm_full_point(&self) -> jubjub::SubgroupPoint {
39        // Calculate the note contents, as bytes
40        let mut asset_generator_bytes = vec![];
41
42        // Write the asset generator, cofactor not cleared
43        asset_generator_bytes.extend_from_slice(&self.generator.to_bytes());
44
45        assert_eq!(asset_generator_bytes.len(), 32);
46
47        // Compute the Pedersen hash of the note contents
48        pedersen_hash(
49            Personalization::NoteCommitment,
50            asset_generator_bytes
51                .into_iter()
52                .flat_map(|byte| (0..8).map(move |i| ((byte >> i) & 1) == 1)),
53        )
54    }
55
56    /// Computes the note commitment
57    pub fn cmu(&self) -> bls12_381::Scalar {
58        // The commitment is in the prime order subgroup, so mapping the
59        // commitment to the u-coordinate is an injective encoding.
60        jubjub::ExtendedPoint::from(self.cm_full_point())
61            .to_affine()
62            .get_u()
63    }
64
65    /// Computes the value commitment for a given amount and randomness
66    pub fn value_commitment(&self, value: u64, randomness: jubjub::Fr) -> ValueCommitment {
67        ValueCommitment {
68            asset_generator: self.generator,
69            value,
70            randomness,
71        }
72    }
73    /// Returns [`self.cmu`] in the correct representation for inclusion in the MASP
74    /// AllowedConversions commitment tree.
75    pub fn commitment(&self) -> Node {
76        Node::from_scalar(self.cmu())
77    }
78}
79
80impl From<AllowedConversion> for I128Sum {
81    fn from(allowed_conversion: AllowedConversion) -> I128Sum {
82        allowed_conversion.assets
83    }
84}
85
86impl From<I128Sum> for AllowedConversion {
87    /// Produces an asset generator without cofactor cleared
88    fn from(assets: I128Sum) -> Self {
89        let mut asset_generator = jubjub::ExtendedPoint::identity();
90        for (asset, value) in assets.components() {
91            // Compute the absolute value (failing if -i64::MAX is
92            // the value)
93            let abs = match value.checked_abs() {
94                Some(a) => a as u64,
95                None => panic!("invalid conversion"),
96            };
97
98            // Is it negative? We'll have to negate later if so.
99            let is_negative = value.is_negative();
100
101            // Compute it in the exponent
102            let mut value_balance = asset.asset_generator() * jubjub::Fr::from(abs);
103
104            // Negate if necessary
105            if is_negative {
106                value_balance = -value_balance;
107            }
108
109            // Add to asset generator
110            asset_generator += value_balance;
111        }
112        AllowedConversion {
113            assets,
114            generator: asset_generator,
115        }
116    }
117}
118
119impl BorshSchema for AllowedConversion {
120    fn add_definitions_recursively(definitions: &mut BTreeMap<Declaration, Definition>) {
121        let definition = Definition::Struct {
122            fields: Fields::NamedFields(vec![
123                ("assets".into(), I128Sum::declaration()),
124                ("generator".into(), <[u8; 32]>::declaration()),
125            ]),
126        };
127        add_definition(Self::declaration(), definition, definitions);
128        I128Sum::add_definitions_recursively(definitions);
129        <[u8; 32]>::add_definitions_recursively(definitions);
130    }
131
132    fn declaration() -> Declaration {
133        "AllowedConversion".into()
134    }
135}
136
137impl BorshSerialize for AllowedConversion {
138    fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
139        self.assets.write(writer)?;
140        writer.write_all(&self.generator.to_bytes())?;
141        Ok(())
142    }
143}
144
145impl BorshDeserialize for AllowedConversion {
146    fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
147        // Use the unchecked reader to ensure that same format is supported
148        let unchecked_conv = UncheckedAllowedConversion::deserialize_reader(reader)?.0;
149        // Recompute the generator using only the value sum
150        let safe_conv: AllowedConversion = unchecked_conv.assets.clone().into();
151        // Check that the computed generator is identical to what was read
152        if safe_conv.generator == unchecked_conv.generator {
153            Ok(safe_conv)
154        } else {
155            // The generators do not match, so the bytes cannot be from Self::serialize
156            Err(io::Error::from(io::ErrorKind::InvalidData))
157        }
158    }
159}
160
161impl Add for AllowedConversion {
162    type Output = Self;
163
164    fn add(self, rhs: Self) -> Self {
165        Self {
166            assets: self.assets + rhs.assets,
167            generator: self.generator + rhs.generator,
168        }
169    }
170}
171
172impl AddAssign for AllowedConversion {
173    fn add_assign(&mut self, rhs: Self) {
174        self.assets += rhs.assets;
175        self.generator += rhs.generator;
176    }
177}
178
179impl Sub for AllowedConversion {
180    type Output = Self;
181
182    fn sub(self, rhs: Self) -> Self {
183        Self {
184            assets: self.assets - rhs.assets,
185            generator: self.generator - rhs.generator,
186        }
187    }
188}
189
190impl SubAssign for AllowedConversion {
191    fn sub_assign(&mut self, rhs: Self) {
192        self.assets -= rhs.assets;
193        self.generator -= rhs.generator;
194    }
195}
196
197impl Neg for AllowedConversion {
198    type Output = Self;
199
200    fn neg(self) -> Self {
201        Self {
202            assets: -self.assets,
203            generator: -self.generator,
204        }
205    }
206}
207
208impl Sum for AllowedConversion {
209    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
210        iter.fold(AllowedConversion::from(ValueSum::zero()), Add::add)
211    }
212}
213
214/// A seprate type to allow unchecked deserializations of AllowedConversions
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct UncheckedAllowedConversion(pub AllowedConversion);
217
218impl BorshDeserialize for UncheckedAllowedConversion {
219    /// This deserialization is unchecked because it does not do the expensive
220    /// computation of checking whether the asset generator corresponds to the
221    /// deserialized amount.
222    fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
223        let assets = I128Sum::read(reader)?;
224        let gen_bytes =
225            <<jubjub::ExtendedPoint as GroupEncoding>::Repr as BorshDeserialize>::deserialize_reader(reader)?;
226        let generator = Option::from(jubjub::ExtendedPoint::from_bytes(&gen_bytes))
227            .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?;
228        // Assume that the generator just read corresponds to the value sum
229        Ok(Self(AllowedConversion { assets, generator }))
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use crate::asset_type::AssetType;
236    use crate::convert::AllowedConversion;
237    use crate::transaction::components::amount::ValueSum;
238
239    /// Generate ZEC asset type
240    fn zec() -> AssetType {
241        AssetType::new(b"ZEC").unwrap()
242    }
243    /// Generate BTC asset type
244    fn btc() -> AssetType {
245        AssetType::new(b"BTC").unwrap()
246    }
247    /// Generate XAN asset type
248    fn xan() -> AssetType {
249        AssetType::new(b"XAN").unwrap()
250    }
251    #[test]
252    fn test_homomorphism() {
253        // Left operand
254        let a = ValueSum::from_pair(zec(), 5i128)
255            + ValueSum::from_pair(btc(), 6i128)
256            + ValueSum::from_pair(xan(), 7i128);
257        // Right operand
258        let b = ValueSum::from_pair(zec(), 2i128) + ValueSum::from_pair(xan(), 10i128);
259        // Test homomorphism
260        assert_eq!(
261            AllowedConversion::from(a.clone() + b.clone()),
262            AllowedConversion::from(a) + AllowedConversion::from(b)
263        );
264    }
265    #[test]
266    fn test_serialization() {
267        // Make conversion
268        let a: AllowedConversion = (ValueSum::from_pair(zec(), 5i128)
269            + ValueSum::from_pair(btc(), 6i128)
270            + ValueSum::from_pair(xan(), 7i128))
271        .into();
272        // Serialize conversion
273        let mut data = Vec::new();
274        use borsh::BorshSerialize;
275        a.serialize(&mut data).unwrap();
276        // Deserialize conversion
277        let mut ptr = &data[..];
278        use borsh::BorshDeserialize;
279        let b = AllowedConversion::deserialize(&mut ptr).unwrap();
280        // Check that all bytes have been finished
281        assert!(
282            ptr.is_empty(),
283            "AllowedConversion bytes should be exhausted"
284        );
285        // Test that serializing then deserializing produces same object
286        assert_eq!(
287            a, b,
288            "serialization followed by deserialization changes value"
289        );
290    }
291}