zebra_chain/sprout/
joinsplit.rs

1//! Sprout funds transfers using [`JoinSplit`]s.
2
3use std::{fmt, io};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    amount::{Amount, NegativeAllowed, NonNegative},
9    block::MAX_BLOCK_BYTES,
10    fmt::HexDebug,
11    primitives::{x25519, Bctv14Proof, Groth16Proof, ZkSnarkProof},
12    serialization::{
13        ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize,
14        ZcashDeserializeInto, ZcashSerialize,
15    },
16};
17
18use super::{commitment, note, tree};
19
20/// A 256-bit seed that must be chosen independently at
21/// random for each [JoinSplit description].
22///
23/// [JoinSplit description]: https://zips.z.cash/protocol/protocol.pdf#joinsplitencodingandconsensus
24#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[cfg_attr(
26    any(test, feature = "proptest-impl"),
27    derive(proptest_derive::Arbitrary)
28)]
29pub struct RandomSeed(HexDebug<[u8; 32]>);
30
31impl RandomSeed {
32    /// Return the bytes in big-endian byte order as required
33    /// by RPCs such as `getrawtransaction`.
34    pub fn bytes_in_display_order(&self) -> [u8; 32] {
35        let mut root: [u8; 32] = self.into();
36        root.reverse();
37        root
38    }
39}
40
41impl From<[u8; 32]> for RandomSeed {
42    fn from(bytes: [u8; 32]) -> Self {
43        Self(bytes.into())
44    }
45}
46
47impl From<RandomSeed> for [u8; 32] {
48    fn from(rt: RandomSeed) -> [u8; 32] {
49        *rt.0
50    }
51}
52
53impl From<&RandomSeed> for [u8; 32] {
54    fn from(random_seed: &RandomSeed) -> Self {
55        random_seed.clone().into()
56    }
57}
58
59/// A _JoinSplit Description_, as described in [protocol specification §7.2][ps].
60///
61/// [ps]: https://zips.z.cash/protocol/protocol.pdf#joinsplitencoding
62#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
63pub struct JoinSplit<P: ZkSnarkProof> {
64    /// A value that the JoinSplit transfer removes from the transparent value
65    /// pool.
66    pub vpub_old: Amount<NonNegative>,
67    /// A value that the JoinSplit transfer inserts into the transparent value
68    /// pool.
69    pub vpub_new: Amount<NonNegative>,
70    /// A root of the Sprout note commitment tree at some block height in the
71    /// past, or the root produced by a previous JoinSplit transfer in this
72    /// transaction.
73    pub anchor: tree::Root,
74    /// A nullifier for the input notes.
75    pub nullifiers: [note::Nullifier; 2],
76    /// A note commitment for this output note.
77    pub commitments: [commitment::NoteCommitment; 2],
78    /// An X25519 public key.
79    pub ephemeral_key: x25519::PublicKey,
80    /// A 256-bit seed that must be chosen independently at random for each
81    /// JoinSplit description.
82    pub random_seed: RandomSeed,
83    /// A message authentication tag.
84    pub vmacs: [note::Mac; 2],
85    /// A ZK JoinSplit proof, either a
86    /// [`Groth16Proof`] or a [`Bctv14Proof`].
87    #[serde(bound(serialize = "P: ZkSnarkProof", deserialize = "P: ZkSnarkProof"))]
88    pub zkproof: P,
89    /// A ciphertext component for this output note.
90    pub enc_ciphertexts: [note::EncryptedNote; 2],
91}
92
93/// The same as `JoinSplit` but with a serialized proof, so that the type
94/// isn't generic anymore.
95#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
96pub struct GenericJoinSplit {
97    /// A value that the JoinSplit transfer removes from the transparent value
98    /// pool.
99    pub vpub_old: Amount<NonNegative>,
100    /// A value that the JoinSplit transfer inserts into the transparent value
101    /// pool.
102    pub vpub_new: Amount<NonNegative>,
103    /// A root of the Sprout note commitment tree at some block height in the
104    /// past, or the root produced by a previous JoinSplit transfer in this
105    /// transaction.
106    pub anchor: tree::Root,
107    /// A nullifier for the input notes.
108    pub nullifiers: [note::Nullifier; 2],
109    /// A note commitment for this output note.
110    pub commitments: [commitment::NoteCommitment; 2],
111    /// An X25519 public key.
112    pub ephemeral_key: x25519::PublicKey,
113    /// A 256-bit seed that must be chosen independently at random for each
114    /// JoinSplit description.
115    pub random_seed: RandomSeed,
116    /// A message authentication tag.
117    pub vmacs: [note::Mac; 2],
118    /// A ZK JoinSplit proof, either a serialized
119    /// [`Groth16Proof`] or a [`Bctv14Proof`].
120    pub zkproof: Option<Vec<u8>>,
121    /// A ciphertext component for this output note.
122    pub enc_ciphertexts: [note::EncryptedNote; 2],
123}
124
125impl<P: ZkSnarkProof> fmt::Debug for JoinSplit<P> {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        f.debug_struct("JoinSplit")
128            .field("vpub_old", &self.vpub_old)
129            .field("vpub_new", &self.vpub_new)
130            .field("anchor", &self.anchor)
131            .field("nullifiers", &self.nullifiers)
132            .field("commitments", &self.commitments)
133            .field("ephemeral_key", &HexDebug(self.ephemeral_key.as_bytes()))
134            .field("random_seed", &self.random_seed)
135            .field("vmacs", &self.vmacs)
136            .field("zkproof", &self.zkproof)
137            .field("enc_ciphertexts", &self.enc_ciphertexts)
138            .finish()
139    }
140}
141
142impl<P: ZkSnarkProof> ZcashSerialize for JoinSplit<P> {
143    fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
144        self.vpub_old.zcash_serialize(&mut writer)?;
145        self.vpub_new.zcash_serialize(&mut writer)?;
146
147        writer.write_32_bytes(&self.anchor.into())?;
148        writer.write_32_bytes(&self.nullifiers[0].into())?;
149        writer.write_32_bytes(&self.nullifiers[1].into())?;
150        writer.write_32_bytes(&self.commitments[0].into())?;
151        writer.write_32_bytes(&self.commitments[1].into())?;
152
153        writer.write_all(&self.ephemeral_key.as_bytes()[..])?;
154        // The borrow is actually needed to avoid taking ownership
155        #[allow(clippy::needless_borrow)]
156        writer.write_32_bytes(&(&self.random_seed).into())?;
157
158        self.vmacs[0].zcash_serialize(&mut writer)?;
159        self.vmacs[1].zcash_serialize(&mut writer)?;
160        self.zkproof.zcash_serialize(&mut writer)?;
161
162        self.enc_ciphertexts[0].zcash_serialize(&mut writer)?;
163        self.enc_ciphertexts[1].zcash_serialize(&mut writer)?;
164
165        Ok(())
166    }
167}
168
169impl<P: ZkSnarkProof> JoinSplit<P> {
170    /// Return the sprout value balance,
171    /// the change in the transaction value pool due to this sprout [`JoinSplit`].
172    ///
173    /// <https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions>
174    ///
175    /// See [`sprout_value_balance`][svb] for details.
176    ///
177    /// [svb]: crate::transaction::Transaction::sprout_value_balance
178    pub fn value_balance(&self) -> Amount<NegativeAllowed> {
179        let vpub_new = self
180            .vpub_new
181            .constrain()
182            .expect("constrain::NegativeAllowed is always valid");
183        let vpub_old = self
184            .vpub_old
185            .constrain()
186            .expect("constrain::NegativeAllowed is always valid");
187
188        (vpub_new - vpub_old).expect("subtraction of two valid amounts is a valid NegativeAllowed")
189    }
190}
191
192impl<P: ZkSnarkProof> ZcashDeserialize for JoinSplit<P> {
193    fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
194        // # Consensus
195        //
196        // > Elements of a JoinSplit description MUST have the types given above
197        //
198        // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
199        //
200        // See comments below for each specific type.
201        Ok(JoinSplit::<P> {
202            // Type is `{0 .. MAX_MONEY}`; see [`NonNegative::valid_range()`].
203            vpub_old: (&mut reader).zcash_deserialize_into()?,
204            vpub_new: (&mut reader).zcash_deserialize_into()?,
205            // Type is `B^{ℓ^{Sprout}_{Merkle}}` i.e. 32 bytes.
206            anchor: tree::Root::from(reader.read_32_bytes()?),
207            // Types are `B^{ℓ^{Sprout}_{PRF}}` i.e. 32 bytes.
208            nullifiers: [
209                reader.read_32_bytes()?.into(),
210                reader.read_32_bytes()?.into(),
211            ],
212            // Types are `NoteCommit^{Sprout}.Output`, i.e. `B^{ℓ^{Sprout}_{Merkle}}`,
213            // i.e. 32 bytes. https://zips.z.cash/protocol/protocol.pdf#abstractcommit
214            commitments: [
215                commitment::NoteCommitment::from(reader.read_32_bytes()?),
216                commitment::NoteCommitment::from(reader.read_32_bytes()?),
217            ],
218            // Type is `KA^{Sprout}.Public`, i.e. `B^Y^{[32]}`, i.e. 32 bytes.
219            // https://zips.z.cash/protocol/protocol.pdf#concretesproutkeyagreement
220            ephemeral_key: x25519_dalek::PublicKey::from(reader.read_32_bytes()?),
221            // Type is `B^{[ℓ_{Seed}]}`, i.e. 32 bytes
222            random_seed: RandomSeed::from(reader.read_32_bytes()?),
223            // Types are `B^{ℓ^{Sprout}_{PRF}}` i.e. 32 bytes.
224            // See [`note::Mac::zcash_deserialize`].
225            vmacs: [
226                note::Mac::zcash_deserialize(&mut reader)?,
227                note::Mac::zcash_deserialize(&mut reader)?,
228            ],
229            // Type is described in https://zips.z.cash/protocol/protocol.pdf#grothencoding.
230            // It is not enforced here; this just reads 192 bytes.
231            // The type is validated when validating the proof, see
232            // [`groth16::Item::try_from`]. In #3179 we plan to validate here instead.
233            zkproof: P::zcash_deserialize(&mut reader)?,
234            // Types are `Sym.C`, i.e. `B^Y^{\[N\]}`, i.e. arbitrary-sized byte arrays
235            // https://zips.z.cash/protocol/protocol.pdf#concretesym but fixed to
236            // 601 bytes in https://zips.z.cash/protocol/protocol.pdf#joinsplitencodingandconsensus
237            // See [`note::EncryptedNote::zcash_deserialize`].
238            enc_ciphertexts: [
239                note::EncryptedNote::zcash_deserialize(&mut reader)?,
240                note::EncryptedNote::zcash_deserialize(&mut reader)?,
241            ],
242        })
243    }
244}
245
246/// The size of a joinsplit, excluding the ZkProof
247///
248/// Excluding the ZkProof, a Joinsplit consists of an 8 byte vpub_old, an 8 byte vpub_new, a 32 byte anchor,
249/// two 32 byte nullifiers, two 32 byte commitments, a 32 byte ephemeral key, a 32 byte random seed
250/// two 32 byte vmacs, and two 601 byte encrypted ciphertexts.
251const JOINSPLIT_SIZE_WITHOUT_ZKPROOF: u64 =
252    8 + 8 + 32 + (32 * 2) + (32 * 2) + 32 + 32 + (32 * 2) + (601 * 2);
253/// The size of a version 2 or 3 joinsplit transaction, which uses a BCTV14 proof.
254///
255/// A BTCV14 proof takes 296 bytes, per the Zcash [protocol specification §7.2][ps]
256///
257/// [ps]: https://zips.z.cash/protocol/protocol.pdf#joinsplitencoding
258pub(crate) const BCTV14_JOINSPLIT_SIZE: u64 = JOINSPLIT_SIZE_WITHOUT_ZKPROOF + 296;
259/// The size of a version 4+ joinsplit transaction, which uses a Groth16 proof
260///
261/// A Groth16 proof takes 192 bytes, per the Zcash [protocol specification §7.2][ps]
262///
263/// [ps]: https://zips.z.cash/protocol/protocol.pdf#joinsplitencoding
264pub(crate) const GROTH16_JOINSPLIT_SIZE: u64 = JOINSPLIT_SIZE_WITHOUT_ZKPROOF + 192;
265
266impl TrustedPreallocate for JoinSplit<Bctv14Proof> {
267    fn max_allocation() -> u64 {
268        // The longest Vec<JoinSplit> we receive from an honest peer must fit inside a valid block.
269        // Since encoding the length of the vec takes at least one byte
270        // (MAX_BLOCK_BYTES - 1) / BCTV14_JOINSPLIT_SIZE is a loose upper bound on the max allocation
271        (MAX_BLOCK_BYTES - 1) / BCTV14_JOINSPLIT_SIZE
272    }
273}
274
275impl TrustedPreallocate for JoinSplit<Groth16Proof> {
276    // The longest Vec<JoinSplit> we receive from an honest peer must fit inside a valid block.
277    // Since encoding the length of the vec takes at least one byte
278    // (MAX_BLOCK_BYTES - 1) / GROTH16_JOINSPLIT_SIZE is a loose upper bound on the max allocation
279    fn max_allocation() -> u64 {
280        (MAX_BLOCK_BYTES - 1) / GROTH16_JOINSPLIT_SIZE
281    }
282}
283
284impl<P> From<JoinSplit<P>> for GenericJoinSplit
285where
286    P: ZkSnarkProof,
287{
288    fn from(val: JoinSplit<P>) -> Self {
289        GenericJoinSplit {
290            vpub_old: val.vpub_old,
291            vpub_new: val.vpub_new,
292            anchor: val.anchor,
293            nullifiers: val.nullifiers,
294            commitments: val.commitments,
295            ephemeral_key: val.ephemeral_key,
296            random_seed: val.random_seed,
297            vmacs: val.vmacs,
298            zkproof: val.zkproof.zcash_serialize_to_vec().ok(),
299            enc_ciphertexts: val.enc_ciphertexts,
300        }
301    }
302}