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}