ledger_zcash_builder/txbuilder/
builder_data.rs

1//! This module mostly contains data structures that are originally present in
2//! the zcash_primitives crate but have been adapted to be HSM compatible
3
4use std::io::{self, Write};
5
6use chacha20poly1305::{
7    aead::{Aead, NewAead},
8    ChaCha20Poly1305, Key, Nonce,
9};
10use group::{cofactor::CofactorGroup, GroupEncoding};
11use jubjub::SubgroupPoint;
12use rand::{CryptoRng, RngCore};
13use sha2::{Digest, Sha256};
14use zcash_note_encryption::NoteEncryption;
15use zcash_primitives::{
16    consensus,
17    keys::OutgoingViewingKey,
18    legacy::{Script, TransparentAddress},
19    memo::MemoBytes as Memo,
20    merkle_tree::MerklePath,
21    sapling::{
22        note_encryption::sapling_note_encryption, Diversifier, Node, Note, PaymentAddress, ProofGenerationKey, Rseed,
23    },
24    transaction::{
25        self,
26        components::{sapling, transparent, Amount, OutPoint, TxIn, TxOut, GROTH_PROOF_SIZE},
27        sighash::{signature_hash, SignableInput, SIGHASH_ALL},
28        TransactionData,
29    },
30};
31
32use crate::{data::HashSeed, errors::Error, txbuilder::hsmauth, txprover::HsmTxProver};
33
34const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d
35    32; // esk
36const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + 16;
37
38#[derive(educe::Educe, Clone)]
39#[educe(Debug)]
40pub struct SpendDescriptionInfo {
41    // extsk: ExtendedSpendingKey, //change this to path in ledger
42    pub diversifier: Diversifier,
43    pub note: Note,
44    pub alpha: jubjub::Fr,
45    // get both from ledger and generate self
46    pub merkle_path: MerklePath<Node>,
47    #[educe(Debug(ignore))]
48    pub proofkey: ProofGenerationKey,
49    // get from ledger
50    pub rcv: jubjub::Fr,
51}
52
53#[derive(Clone)]
54pub struct SaplingOutput {
55    /// `None` represents the `ovk = ⊥` case.
56    pub ovk: Option<OutgoingViewingKey>,
57    // get from ledger
58    pub to: PaymentAddress,
59    pub note: Note,
60    pub memo: Memo,
61    pub rcv: jubjub::Fr, // get from ledger
62    pub hashseed: Option<HashSeed>,
63}
64
65impl SaplingOutput {
66    pub fn new<R: RngCore + CryptoRng, P: consensus::Parameters>(
67        ovk: Option<OutgoingViewingKey>,
68        to: PaymentAddress,
69        value: Amount,
70        memo: Option<Memo>,
71        rcv: jubjub::Fr,
72        rseed: Rseed,
73        hashseed: Option<HashSeed>,
74    ) -> Result<Self, Error> {
75        let g_d = match to.g_d() {
76            Some(g_d) => g_d,
77            None => return Err(Error::InvalidAddress),
78        };
79        if value.is_negative() {
80            return Err(Error::InvalidAmount);
81        }
82
83        // let rseed = generate_random_rseed::<P, R>(height, rng);
84
85        let note = Note { g_d, pk_d: *to.pk_d(), value: value.into(), rseed };
86
87        Ok(SaplingOutput { ovk, to, note, memo: memo.unwrap_or_else(Memo::empty), rcv, hashseed })
88    }
89
90    pub fn build<P: consensus::Parameters, PR: HsmTxProver, R: RngCore + CryptoRng>(
91        self,
92        prover: &PR,
93        ctx: &mut PR::SaplingProvingContext,
94        rng: &mut R,
95        params: &P,
96    ) -> transaction::components::OutputDescription<<hsmauth::sapling::Unauthorized as sapling::Authorization>::Proof>
97    {
98        let mut encryptor =
99            sapling_note_encryption::<R, P>(self.ovk, self.note.clone(), self.to.clone(), self.memo, rng);
100
101        let (zkproof, cv) =
102            prover.output_proof(ctx, *encryptor.esk(), self.to, self.note.rcm(), self.note.value, self.rcv);
103
104        let cmu = self.note.cmu();
105
106        let enc_ciphertext = encryptor.encrypt_note_plaintext();
107        let out_ciphertext = if self.ovk.is_some() {
108            encryptor.encrypt_outgoing_plaintext(&cv, &cmu, rng)
109        } else {
110            let seed = self.hashseed.unwrap().0;
111            let mut randbytes = [0u8; 32 + OUT_PLAINTEXT_SIZE];
112            for i in 0 .. 3 {
113                let mut sha256 = Sha256::new();
114                sha256.update([i as u8]);
115                sha256.update(seed);
116                let h = sha256.finalize();
117                randbytes[i * 32 .. (i + 1) * 32].copy_from_slice(&h);
118            }
119
120            let ock = Key::from_slice(&randbytes[0 .. 32]);
121            let out_ciphertext = ChaCha20Poly1305::new(ock)
122                .encrypt(Nonce::from_slice(&[0u8; 12]), &randbytes[32 ..])
123                .unwrap();
124
125            assert_eq!(out_ciphertext.len(), OUT_CIPHERTEXT_SIZE);
126
127            let mut array = [0u8; OUT_CIPHERTEXT_SIZE];
128            array.copy_from_slice(&out_ciphertext);
129            array
130        };
131
132        let ephemeral_key = encryptor.epk().to_bytes().into();
133
134        transaction::components::OutputDescription { cv, cmu, ephemeral_key, enc_ciphertext, out_ciphertext, zkproof }
135    }
136}
137
138#[derive(Debug, Clone)]
139pub struct TransparentInputInfo {
140    pub pubkey: secp256k1::PublicKey,
141    pub coin: TxOut,
142}
143
144/// Metadata about a transaction created by a [`crate::Builder`].
145#[derive(Debug, PartialEq, Clone, Default)]
146pub struct SaplingMetadata {
147    pub(crate) spend_indices: Vec<usize>,
148    pub(crate) output_indices: Vec<usize>,
149}
150
151impl SaplingMetadata {
152    pub fn new() -> Self {
153        Default::default()
154    }
155
156    /// Returns the index within the transaction of the [`SpendDescription`]
157    /// corresponding to the `n`-th call to
158    /// [`crate::Builder::add_sapling_spend`].
159    ///
160    /// Note positions are randomized when building transactions for
161    /// indistinguishability. This means that the transaction consumer
162    /// cannot assume that e.g. the first spend they added (via the first
163    /// call to [`crate::Builder::add_sapling_spend`]) is the first
164    /// [`SpendDescription`] in the transaction.
165    pub fn spend_index(
166        &self,
167        n: usize,
168    ) -> Option<usize> {
169        self.spend_indices.get(n).copied()
170    }
171
172    /// Returns the index within the transaction of the [`OutputDescription`]
173    /// corresponding to the `n`-th call to
174    /// [`crate::Builder::add_sapling_output`].
175    ///
176    /// Note positions are randomized when building transactions for
177    /// indistinguishability. This means that the transaction consumer
178    /// cannot assume that e.g. the first output they added (via the first
179    /// call to [`crate::Builder::add_sapling_output`]) is the first
180    /// [`OutputDescription`] in the transaction.
181    pub fn output_index(
182        &self,
183        n: usize,
184    ) -> Option<usize> {
185        self.output_indices.get(n).copied()
186    }
187}
188
189impl From<sapling::builder::SaplingMetadata> for SaplingMetadata {
190    fn from(tx_meta: sapling::builder::SaplingMetadata) -> Self {
191        let mut spends = vec![];
192        let mut outputs = vec![];
193
194        let mut i = 0;
195        while let Some(ix) = tx_meta.spend_index(i) {
196            spends.push(ix);
197            i += 1;
198        }
199
200        i = 0;
201        while let Some(ix) = tx_meta.output_index(i) {
202            outputs.push(ix);
203            i += 1;
204        }
205
206        Self { spend_indices: spends, output_indices: outputs }
207    }
208}
209
210#[derive(Clone)]
211pub struct NullifierInput {
212    pub rcm_old: [u8; 32],
213    pub note_position: [u8; 8],
214}
215
216impl NullifierInput {
217    pub fn write<W: Write>(
218        &self,
219        mut writer: W,
220    ) -> io::Result<()> {
221        writer.write_all(&self.rcm_old)?;
222        writer.write_all(&self.note_position)
223    }
224}
225
226#[derive(Clone)]
227pub struct TransparentScriptData {
228    pub prevout: [u8; 36],
229    pub script_pubkey: [u8; 26],
230    pub value: [u8; 8],
231    pub sequence: [u8; 4],
232}
233
234impl TransparentScriptData {
235    pub fn write<W: Write>(
236        &self,
237        mut writer: W,
238    ) -> io::Result<()> {
239        writer.write_all(&self.prevout)?;
240        writer.write_all(&self.script_pubkey)?;
241        writer.write_all(&self.value)?;
242        writer.write_all(&self.sequence)
243    }
244}
245
246#[derive(Clone)]
247pub struct SpendDescription {
248    pub cv: [u8; 32],
249    pub anchor: [u8; 32],
250    pub nullifier: [u8; 32],
251    pub rk: [u8; 32],
252    pub zkproof: [u8; GROTH_PROOF_SIZE],
253}
254
255impl SpendDescription {
256    pub fn from(info: &sapling::SpendDescription<hsmauth::sapling::Unauthorized>) -> SpendDescription {
257        SpendDescription {
258            cv: info.cv.to_bytes(),
259            anchor: info.anchor.to_bytes(),
260            nullifier: info.nullifier.0,
261            rk: info.rk.0.to_bytes(),
262            zkproof: info.zkproof,
263        }
264    }
265
266    pub fn write<W: Write>(
267        &self,
268        mut writer: W,
269    ) -> io::Result<()> {
270        writer.write_all(&self.cv)?;
271        writer.write_all(&self.anchor)?;
272        writer.write_all(&self.nullifier)?;
273        writer.write_all(&self.rk)?;
274        writer.write_all(&self.zkproof)
275    }
276}
277
278#[derive(Clone)]
279pub struct OutputDescription {
280    pub cv: [u8; 32],
281    pub cmu: [u8; 32],
282    pub ephemeral_key: [u8; 32],
283    pub enc_ciphertext: [u8; 580],
284    pub out_ciphertext: [u8; 80],
285    pub zkproof: [u8; GROTH_PROOF_SIZE],
286}
287
288impl
289    From<&transaction::components::OutputDescription<<hsmauth::sapling::Unauthorized as sapling::Authorization>::Proof>>
290    for OutputDescription
291{
292    fn from(
293        from: &transaction::components::OutputDescription<
294            <hsmauth::sapling::Unauthorized as sapling::Authorization>::Proof,
295        >
296    ) -> Self {
297        Self {
298            cv: from.cv.to_bytes(),
299            cmu: from.cmu.to_bytes(),
300            ephemeral_key: from.ephemeral_key.0,
301            enc_ciphertext: from.enc_ciphertext,
302            out_ciphertext: from.out_ciphertext,
303            zkproof: from.zkproof,
304        }
305    }
306}
307
308impl OutputDescription {
309    pub fn write<W: Write>(
310        &self,
311        mut writer: W,
312    ) -> io::Result<()> {
313        writer.write_all(&self.cv)?;
314        writer.write_all(&self.cmu)?;
315        writer.write_all(&self.ephemeral_key)?;
316        writer.write_all(&self.enc_ciphertext)?;
317        writer.write_all(&self.out_ciphertext)?;
318        writer.write_all(&self.zkproof)
319    }
320}
321
322/// Converts a zcash_primitives' SpendDescription to the HSM-compatible format
323pub fn spend_data_hms_fromtx(
324    input: &[sapling::SpendDescription<hsmauth::sapling::Unauthorized>]
325) -> Vec<SpendDescription> {
326    let mut data = Vec::new();
327    for info in input.iter() {
328        let description = SpendDescription::from(info);
329        data.push(description);
330    }
331    data
332}
333
334/// Converts a zcash_primitives' OutputDescription to the HSM-compatible format
335pub fn output_data_hsm_fromtx(
336    input: &[sapling::OutputDescription<sapling::GrothProofBytes>]
337) -> Vec<OutputDescription> {
338    let mut data = Vec::new();
339    for info in input.iter() {
340        let description = OutputDescription::from(info);
341        data.push(description);
342    }
343    data
344}
345
346/// Converts a list of [`SpendDescriptionInfo`] to a vec of [`NullifierInput`]s
347pub fn spend_old_data_fromtx(data: &[SpendDescriptionInfo]) -> Vec<NullifierInput> {
348    let mut v = Vec::new();
349    for info in data.iter() {
350        let n = NullifierInput {
351            rcm_old: info.note.rcm().to_bytes(),
352            note_position: info.merkle_path.position.to_le_bytes(),
353        };
354        v.push(n);
355    }
356    v
357}
358
359/// Generates a list of [`TransparentScriptData`] from
360/// a list of [`TransparentInputInfo`] and `TransactionData`
361pub fn transparent_script_data_fromtx<A: transparent::Authorization>(
362    vins: &[TxIn<A>],
363    inputs: &[TransparentInputInfo],
364) -> Result<Vec<TransparentScriptData>, Error> {
365    let mut data = Vec::new();
366    for (i, (info, vin)) in inputs.iter().zip(vins).enumerate() {
367        let mut prevout = [0u8; 36];
368        prevout[0 .. 32].copy_from_slice(vin.prevout.hash().as_ref());
369        prevout[32 .. 36].copy_from_slice(&vin.prevout.n().to_le_bytes());
370
371        let mut script_pubkey = [0u8; 26];
372        info.coin
373            .script_pubkey
374            .write(&mut script_pubkey[..])?;
375
376        let mut value = [0u8; 8];
377        value.copy_from_slice(&info.coin.value.to_i64_le_bytes());
378
379        let mut sequence = [0u8; 4];
380        sequence.copy_from_slice(&vin.sequence.to_le_bytes());
381
382        let ts = TransparentScriptData { prevout, script_pubkey, value, sequence };
383        data.push(ts);
384    }
385    Ok(data)
386}