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}