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