Skip to main content

dusk_core/transfer/
phoenix.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7//! Types related to the phoenix transaction model of Dusk's transfer contract.
8
9use alloc::vec::Vec;
10use core::cmp;
11use core::fmt::Debug;
12
13use bytecheck::CheckBytes;
14use dusk_bytes::{DeserializableSlice, Error as BytesError, Serializable};
15use dusk_poseidon::{Domain, Hash};
16use ff::Field;
17// phoenix types
18pub use phoenix_circuits::{InputNoteInfo, OutputNoteInfo, TxCircuit};
19pub use phoenix_core::{
20    Error as CoreError, NOTE_VAL_ENC_SIZE, Note, OUTPUT_NOTES, PublicKey,
21    SecretKey, Sender, StealthAddress, TxSkeleton, ViewKey, value_commitment,
22};
23use rand::{CryptoRng, RngCore};
24use rkyv::{Archive, Deserialize, Serialize};
25
26use crate::signatures::schnorr::{
27    SecretKey as SchnorrSecretKey, Signature as SchnorrSignature,
28};
29use crate::transfer::data::{
30    BlobData, ContractBytecode, ContractCall, ContractDeploy, MAX_MEMO_SIZE,
31    TransactionData,
32};
33use crate::{BlsScalar, Error, JubJubAffine, JubJubScalar};
34
35/// The depth of the merkle tree of notes stored in the transfer-contract.
36pub const NOTES_TREE_DEPTH: usize = 17;
37/// The arity of the merkle tree of notes stored in the transfer-contract.
38pub use poseidon_merkle::ARITY as NOTES_TREE_ARITY;
39/// The merkle tree of notes stored in the transfer-contract.
40pub type NotesTree = poseidon_merkle::Tree<(), NOTES_TREE_DEPTH>;
41/// The merkle opening for a note-hash in the merkle tree of notes.
42pub type NoteOpening = poseidon_merkle::Opening<(), NOTES_TREE_DEPTH>;
43/// the tree item for the merkle-tree of notes stored in the transfer-contract.
44pub type NoteTreeItem = poseidon_merkle::Item<()>;
45
46/// A leaf of the merkle tree of notes stored in the transfer-contract.
47#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
48#[archive_attr(derive(CheckBytes))]
49pub struct NoteLeaf {
50    /// The height of the block when the note was inserted in the tree.
51    pub block_height: u64,
52    /// The note inserted in the tree.
53    pub note: Note,
54}
55
56impl AsRef<Note> for NoteLeaf {
57    fn as_ref(&self) -> &Note {
58        &self.note
59    }
60}
61
62/// Ord compares positions, not values, note values need to be decrypted first
63impl cmp::Ord for NoteLeaf {
64    fn cmp(&self, other: &Self) -> cmp::Ordering {
65        self.note.pos().cmp(other.note.pos())
66    }
67}
68
69impl cmp::PartialOrd for NoteLeaf {
70    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
71        Some(self.cmp(other))
72    }
73}
74
75/// Label used for the ZK transcript initialization. Must be the same for prover
76/// and verifier.
77pub const TRANSCRIPT_LABEL: &[u8] = b"dusk-network";
78
79/// Phoenix transaction.
80#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
81#[archive_attr(derive(CheckBytes))]
82pub struct Transaction {
83    payload: Payload,
84    proof: Vec<u8>,
85}
86
87impl PartialEq for Transaction {
88    fn eq(&self, other: &Self) -> bool {
89        self.hash() == other.hash()
90    }
91}
92
93impl Eq for Transaction {}
94
95impl Transaction {
96    fn from_slice_with(
97        buf: &[u8],
98        parse_payload: fn(&[u8]) -> Result<Payload, BytesError>,
99    ) -> Result<Self, BytesError> {
100        let mut buf = buf;
101        let payload = parse_payload(read_len_prefixed(&mut buf)?)?;
102        let proof = read_len_prefixed(&mut buf)?.into();
103
104        if !buf.is_empty() {
105            return Err(BytesError::InvalidData);
106        }
107
108        Ok(Self { payload, proof })
109    }
110
111    /// Create a new phoenix transaction given the sender secret-key, receiver
112    /// public-key, the input note positions in the transaction tree and the
113    /// new output-notes.
114    ///
115    /// # Errors
116    /// The creation of a transaction is not possible and will error if:
117    /// - one of the input-notes doesn't belong to the `sender_sk`
118    /// - the transaction input doesn't cover the transaction costs
119    /// - the `inputs` vector is either empty or larger than 4 elements
120    /// - the `inputs` vector contains duplicate `Note`s
121    /// - the `prover` is implemented incorrectly
122    /// - the memo, if given, is too large
123    #[allow(clippy::too_many_lines)]
124    #[allow(clippy::too_many_arguments)]
125    #[allow(clippy::similar_names)]
126    pub fn new<R: RngCore + CryptoRng, P: Prove>(
127        rng: &mut R,
128        sender_sk: &SecretKey,
129        refund_pk: &PublicKey,
130        receiver_pk: &PublicKey,
131        inputs: Vec<(Note, NoteOpening)>,
132        root: BlsScalar,
133        transfer_value: u64,
134        obfuscate_transfer_note: bool,
135        deposit: u64,
136        gas_limit: u64,
137        gas_price: u64,
138        chain_id: u8,
139        data: Option<impl Into<TransactionData>>,
140        prover: &P,
141    ) -> Result<Self, Error> {
142        let data = data.map(Into::into);
143
144        if let Some(TransactionData::Memo(memo)) = data.as_ref()
145            && memo.len() > MAX_MEMO_SIZE
146        {
147            return Err(Error::MemoTooLarge(memo.len()));
148        }
149
150        let sender_pk = PublicKey::from(sender_sk);
151        let sender_vk = ViewKey::from(sender_sk);
152
153        // get input note values, value-blinders and nullifiers
154        let input_len = inputs.len();
155        let mut input_values = Vec::with_capacity(input_len);
156        let mut input_value_blinders = Vec::with_capacity(input_len);
157        let mut input_nullifiers = Vec::with_capacity(input_len);
158        for (note, _opening) in &inputs {
159            let note_nullifier = note.gen_nullifier(sender_sk);
160            for nullifier in &input_nullifiers {
161                if note_nullifier == *nullifier {
162                    return Err(Error::Replay);
163                }
164            }
165            input_nullifiers.push(note_nullifier);
166            input_values.push(note.value(Some(&sender_vk))?);
167            input_value_blinders.push(note.value_blinder(Some(&sender_vk))?);
168        }
169        let input_value: u64 = input_values.iter().sum();
170
171        // --- Create the transaction payload
172
173        // Compute max_fee directly so the change note can be created
174        // before the Fee. This lets us derive Fee.stealth_address from
175        // the ZK-proven change note, binding the refund destination to
176        // the proof and preventing stealth-address redirection.
177        let max_fee = gas_limit * gas_price;
178
179        if input_value < transfer_value + max_fee + deposit {
180            return Err(Error::InsufficientBalance);
181        }
182
183        // Generate output notes:
184        let transfer_value_blinder = if obfuscate_transfer_note {
185            JubJubScalar::random(&mut *rng)
186        } else {
187            JubJubScalar::zero()
188        };
189        let transfer_sender_blinder = [
190            JubJubScalar::random(&mut *rng),
191            JubJubScalar::random(&mut *rng),
192        ];
193        let change_sender_blinder = [
194            JubJubScalar::random(&mut *rng),
195            JubJubScalar::random(&mut *rng),
196        ];
197        let transfer_note = if obfuscate_transfer_note {
198            Note::obfuscated(
199                rng,
200                &sender_pk,
201                receiver_pk,
202                transfer_value,
203                transfer_value_blinder,
204                transfer_sender_blinder,
205            )
206        } else {
207            Note::transparent(
208                rng,
209                &sender_pk,
210                receiver_pk,
211                transfer_value,
212                transfer_sender_blinder,
213            )
214        };
215        // The change note should have the value of the input note, minus what
216        // is maximally spent.
217        let change_value = input_value - transfer_value - max_fee - deposit;
218        let change_value_blinder = JubJubScalar::random(&mut *rng);
219        let change_note = Note::obfuscated(
220            rng,
221            &sender_pk,
222            refund_pk,
223            change_value,
224            change_value_blinder,
225            change_sender_blinder,
226        );
227
228        // Derive Fee.stealth_address from the change note so the gas
229        // refund destination is bound to the ZK-proven output.
230        let refund_sender_blinder = [
231            JubJubScalar::random(&mut *rng),
232            JubJubScalar::random(&mut *rng),
233        ];
234        let refund_sa = *change_note.stealth_address();
235        let fee = Fee {
236            gas_limit,
237            gas_price,
238            stealth_address: refund_sa,
239            sender: Sender::encrypt(
240                refund_sa.note_pk(),
241                refund_pk,
242                &refund_sender_blinder,
243            ),
244        };
245
246        let outputs = [transfer_note.clone(), change_note.clone()];
247
248        // Now we can set the tx-skeleton, payload and get the payload-hash
249        let tx_skeleton = TxSkeleton {
250            root,
251            // we also need the nullifiers for the tx-circuit, hence the clone
252            nullifiers: input_nullifiers.clone(),
253            outputs,
254            max_fee,
255            deposit,
256        };
257        let payload = Payload {
258            chain_id,
259            tx_skeleton,
260            fee,
261            data,
262        };
263        let payload_hash = payload.hash();
264
265        // --- Create the transaction proof
266
267        // Create a vector with all the information for the input-notes
268        let mut input_notes_info = Vec::with_capacity(input_len);
269        inputs
270            .into_iter()
271            .zip(input_nullifiers)
272            .zip(input_values)
273            .zip(input_value_blinders)
274            .for_each(
275                |(
276                    (((note, merkle_opening), nullifier), value),
277                    value_blinder,
278                )| {
279                    let note_sk = sender_sk.gen_note_sk(note.stealth_address());
280                    let note_pk_p = JubJubAffine::from(
281                        crate::GENERATOR_NUMS_EXTENDED * note_sk.as_ref(),
282                    );
283                    let signature = note_sk.sign_double(rng, payload_hash);
284                    input_notes_info.push(InputNoteInfo {
285                        merkle_opening,
286                        note,
287                        note_pk_p,
288                        value,
289                        value_blinder,
290                        nullifier,
291                        signature,
292                    });
293                },
294            );
295
296        // Create the information for the output-notes
297        let transfer_value_commitment =
298            value_commitment(transfer_value, transfer_value_blinder);
299        let transfer_note_sender_enc = match transfer_note.sender() {
300            Sender::Encryption(enc) => enc,
301            Sender::ContractInfo(_) => unreachable!("The sender is encrypted"),
302        };
303        let change_value_commitment =
304            value_commitment(change_value, change_value_blinder);
305        let change_note_sender_enc = match change_note.sender() {
306            Sender::Encryption(enc) => enc,
307            Sender::ContractInfo(_) => unreachable!("The sender is encrypted"),
308        };
309        let output_notes_info = [
310            OutputNoteInfo {
311                value: transfer_value,
312                value_commitment: transfer_value_commitment,
313                value_blinder: transfer_value_blinder,
314                note_pk: JubJubAffine::from(
315                    transfer_note.stealth_address().note_pk().as_ref(),
316                ),
317                sender_enc: *transfer_note_sender_enc,
318                sender_blinder: transfer_sender_blinder,
319            },
320            OutputNoteInfo {
321                value: change_value,
322                value_commitment: change_value_commitment,
323                value_blinder: change_value_blinder,
324                note_pk: JubJubAffine::from(
325                    change_note.stealth_address().note_pk().as_ref(),
326                ),
327                sender_enc: *change_note_sender_enc,
328                sender_blinder: change_sender_blinder,
329            },
330        ];
331
332        // Sign the payload hash using both 'a' and 'b' of the sender_sk
333        let schnorr_sk_a = SchnorrSecretKey::from(sender_sk.a());
334        let sig_a = schnorr_sk_a.sign(rng, payload_hash);
335        let schnorr_sk_b = SchnorrSecretKey::from(sender_sk.b());
336        let sig_b = schnorr_sk_b.sign(rng, payload_hash);
337
338        Ok(Self {
339            payload,
340            proof: prover.prove(
341                &TxCircuitVec {
342                    input_notes_info,
343                    output_notes_info,
344                    payload_hash,
345                    root,
346                    deposit,
347                    max_fee,
348                    sender_pk,
349                    signatures: (sig_a, sig_b),
350                }
351                .to_var_bytes(),
352            )?,
353        })
354    }
355
356    /// Creates a new phoenix transaction given the [`Payload`] and proof. Note
357    /// that this function doesn't guarantee that the proof matches the
358    /// payload, if possible use [`Self::new`] instead.
359    #[must_use]
360    pub fn from_payload_and_proof(payload: Payload, proof: Vec<u8>) -> Self {
361        Self { payload, proof }
362    }
363
364    /// Replaces the inner `proof` bytes for a given `proof`.
365    ///
366    /// This can be used to delegate the proof generation after a
367    /// [`Transaction`] is created.
368    /// In order to do that, the transaction would be created using the
369    /// serialized circuit-bytes for the proof-field. Those bytes can be
370    /// sent to a 3rd-party prover-service that generates the proof-bytes
371    /// and sends them back. The proof-bytes will then replace the
372    /// circuit-bytes in the transaction using this function.
373    pub fn set_proof(&mut self, proof: Vec<u8>) {
374        self.proof = proof;
375    }
376
377    /// The proof of the transaction.
378    #[must_use]
379    pub fn proof(&self) -> &[u8] {
380        &self.proof
381    }
382
383    /// The payload-hash of the transaction used as input in the
384    /// phoenix-circuit.
385    #[must_use]
386    pub fn payload_hash(&self) -> BlsScalar {
387        self.payload.hash()
388    }
389
390    /// Returns the nullifiers of the transaction.
391    #[must_use]
392    pub fn nullifiers(&self) -> &[BlsScalar] {
393        &self.payload.tx_skeleton.nullifiers
394    }
395
396    /// Return the root of the notes tree.
397    #[must_use]
398    pub fn root(&self) -> &BlsScalar {
399        &self.payload.tx_skeleton.root
400    }
401
402    /// Return the output notes of the transaction.
403    #[must_use]
404    pub fn outputs(&self) -> &[Note; OUTPUT_NOTES] {
405        &self.payload.tx_skeleton.outputs
406    }
407
408    /// Return the fee for the transaction.
409    #[must_use]
410    pub fn fee(&self) -> &Fee {
411        &self.payload.fee
412    }
413
414    /// Return the stealth address for returning funds for Phoenix transactions
415    /// as specified in the fee.
416    #[must_use]
417    pub fn stealth_address(&self) -> &StealthAddress {
418        &self.payload.fee.stealth_address
419    }
420
421    /// Returns the sender data for Phoenix transactions as specified in the
422    /// fee.
423    #[must_use]
424    pub fn sender(&self) -> &Sender {
425        &self.payload.fee.sender
426    }
427
428    /// Returns the gas limit of the transaction.
429    #[must_use]
430    pub fn gas_limit(&self) -> u64 {
431        self.payload.fee.gas_limit
432    }
433
434    /// Returns the gas price of the transaction.
435    #[must_use]
436    pub fn gas_price(&self) -> u64 {
437        self.payload.fee.gas_price
438    }
439
440    /// Returns the chain ID of the transaction.
441    #[must_use]
442    pub fn chain_id(&self) -> u8 {
443        self.payload.chain_id
444    }
445
446    /// Returns the max fee to be spend by the transaction.
447    #[must_use]
448    pub fn max_fee(&self) -> u64 {
449        self.payload.tx_skeleton.max_fee
450    }
451
452    /// Returns the deposit of the transaction.
453    #[must_use]
454    pub fn deposit(&self) -> u64 {
455        self.payload.tx_skeleton.deposit
456    }
457
458    /// Return the contract call data, if there is any.
459    #[must_use]
460    pub fn call(&self) -> Option<&ContractCall> {
461        #[allow(clippy::match_wildcard_for_single_variants)]
462        match self.data()? {
463            TransactionData::Call(c) => Some(c),
464            _ => None,
465        }
466    }
467
468    /// Return the contract deploy data, if there is any.
469    #[must_use]
470    pub fn deploy(&self) -> Option<&ContractDeploy> {
471        #[allow(clippy::match_wildcard_for_single_variants)]
472        match self.data()? {
473            TransactionData::Deploy(d) => Some(d),
474            _ => None,
475        }
476    }
477
478    /// Return the blob data, if there is any.
479    #[must_use]
480    pub fn blob(&self) -> Option<&Vec<BlobData>> {
481        #[allow(clippy::match_wildcard_for_single_variants)]
482        match self.data()? {
483            TransactionData::Blob(d) => Some(d),
484            _ => None,
485        }
486    }
487
488    /// Return the mutable blob data, if there is any.
489    #[must_use]
490    pub fn blob_mut(&mut self) -> Option<&mut Vec<BlobData>> {
491        #[allow(clippy::match_wildcard_for_single_variants)]
492        match self.data_mut()? {
493            TransactionData::Blob(d) => Some(d),
494            _ => None,
495        }
496    }
497
498    /// Returns the memo used with the transaction, if any.
499    #[must_use]
500    pub fn memo(&self) -> Option<&[u8]> {
501        match self.data()? {
502            TransactionData::Memo(memo) => Some(memo),
503            _ => None,
504        }
505    }
506
507    /// Returns the transaction data, if it exists.
508    #[must_use]
509    pub(crate) fn data(&self) -> Option<&TransactionData> {
510        self.payload.data.as_ref()
511    }
512
513    /// Returns the transaction data, if it exists.
514    #[must_use]
515    pub(crate) fn data_mut(&mut self) -> Option<&mut TransactionData> {
516        self.payload.data.as_mut()
517    }
518
519    /// Creates a modified clone of this transaction if it contains data for
520    /// deployment, clones all fields except for the bytecode' 'bytes' part.
521    /// Returns none if the transaction is not a deployment transaction.
522    #[must_use]
523    pub fn strip_off_bytecode(&self) -> Option<Self> {
524        let deploy = self.deploy()?;
525
526        let stripped_deploy = TransactionData::Deploy(ContractDeploy {
527            owner: deploy.owner.clone(),
528            init_args: deploy.init_args.clone(),
529            bytecode: ContractBytecode {
530                hash: deploy.bytecode.hash,
531                bytes: Vec::new(),
532            },
533            nonce: deploy.nonce,
534        });
535
536        let mut stripped_transaction = self.clone();
537        stripped_transaction.payload.data = Some(stripped_deploy);
538
539        Some(stripped_transaction)
540    }
541
542    /// Creates a modified clone of this transaction if it contains a Blob,
543    /// clones all fields except for the Blob, whose versioned hashes are set as
544    /// Memo.
545    ///
546    /// Returns none if the transaction is not a Blob transaction.
547    #[must_use]
548    pub fn blob_to_memo(&self) -> Option<Self> {
549        let data = self.data()?;
550
551        if let TransactionData::Blob(_) = data {
552            let hash = data.signature_message();
553            let memo = TransactionData::Memo(hash);
554            let mut converted_tx = self.clone();
555            converted_tx.payload.data = Some(memo);
556            Some(converted_tx)
557        } else {
558            None
559        }
560    }
561
562    /// Serialize the `Transaction` into a variable length byte buffer.
563    #[must_use]
564    pub fn to_var_bytes(&self) -> Vec<u8> {
565        let mut bytes = Vec::new();
566
567        let payload_bytes = self.payload.to_var_bytes();
568        bytes.extend((payload_bytes.len() as u64).to_bytes());
569        bytes.extend(payload_bytes);
570
571        bytes.extend((self.proof.len() as u64).to_bytes());
572        bytes.extend(&self.proof);
573
574        bytes
575    }
576
577    /// Deserialize the Transaction from a bytes buffer.
578    ///
579    /// # Errors
580    /// Errors when the bytes are not canonical.
581    pub fn from_slice(buf: &[u8]) -> Result<Self, BytesError> {
582        Self::from_slice_with(buf, Payload::from_slice)
583    }
584
585    /// Deserialize the transaction from bytes using the historical Phoenix
586    /// compatibility parser for finalized ledger data.
587    ///
588    /// # Errors
589    /// Errors when the bytes are not canonical for the historical Phoenix
590    /// ledger-compat parser.
591    pub fn from_slice_ledger_compat(buf: &[u8]) -> Result<Self, BytesError> {
592        Self::from_slice_with(buf, Payload::from_slice_ledger_compat)
593    }
594
595    /// Return input bytes to hash the Transaction.
596    ///
597    /// Note: The result of this function is *only* meant to be used as an input
598    /// for hashing and *cannot* be used to deserialize the `Transaction` again.
599    #[must_use]
600    pub fn to_hash_input_bytes(&self) -> Vec<u8> {
601        let mut bytes = self.payload.to_hash_input_bytes();
602        bytes.extend(&self.proof);
603        bytes
604    }
605
606    /// Create the `Transaction`-hash.
607    #[must_use]
608    pub fn hash(&self) -> BlsScalar {
609        BlsScalar::hash_to_scalar(&self.to_hash_input_bytes())
610    }
611
612    /// Return the public input to be used in the phoenix-transaction circuit
613    /// verification
614    ///
615    /// These are:
616    /// - `payload_hash`
617    /// - `root`
618    /// - `[nullifier; I]`
619    /// - `[output_value_commitment; 2]`
620    /// - `max_fee`
621    /// - `deposit`
622    /// - `(npk_out_0, npk_out_1)`
623    /// - `(enc_A_npk_out_0, enc_B_npk_out_0)`
624    /// - `(enc_A_npk_out_1, enc_B_npk_out_1)`
625    ///
626    /// # Panics
627    /// Panics if one of the output-notes doesn't have the sender set to
628    /// [`Sender::Encryption`].
629    #[must_use]
630    pub fn public_inputs(&self) -> Vec<BlsScalar> {
631        let tx_skeleton = &self.payload.tx_skeleton;
632
633        // retrieve the number of input and output notes
634        let input_len = tx_skeleton.nullifiers.len();
635        let output_len = tx_skeleton.outputs.len();
636
637        let size =
638            // payload-hash and root
639            1 + 1
640            // nullifiers
641            + input_len
642            // output-notes value-commitment
643            + 2 * output_len
644            // max-fee and deposit
645            + 1 + 1
646            // output-notes public-keys
647            + 2 * output_len
648            // sender-encryption for both output-notes
649            + 2 * 4 * output_len;
650        // build the public input vector
651        let mut pis = Vec::<BlsScalar>::with_capacity(size);
652        pis.push(self.payload.hash());
653        pis.push(tx_skeleton.root);
654        pis.extend(tx_skeleton.nullifiers().iter());
655        tx_skeleton.outputs().iter().for_each(|note| {
656            let value_commitment = note.value_commitment();
657            pis.push(value_commitment.get_u());
658            pis.push(value_commitment.get_v());
659        });
660        pis.push(tx_skeleton.max_fee().into());
661        pis.push(tx_skeleton.deposit().into());
662        tx_skeleton.outputs().iter().for_each(|note| {
663            let note_pk =
664                JubJubAffine::from(note.stealth_address().note_pk().as_ref());
665            pis.push(note_pk.get_u());
666            pis.push(note_pk.get_v());
667        });
668        tx_skeleton
669            .outputs()
670            .iter()
671            .for_each(|note| match note.sender() {
672                Sender::Encryption(sender_enc) => {
673                    pis.push(sender_enc[0].0.get_u());
674                    pis.push(sender_enc[0].0.get_v());
675                    pis.push(sender_enc[0].1.get_u());
676                    pis.push(sender_enc[0].1.get_v());
677                    pis.push(sender_enc[1].0.get_u());
678                    pis.push(sender_enc[1].0.get_v());
679                    pis.push(sender_enc[1].1.get_u());
680                    pis.push(sender_enc[1].1.get_v());
681                }
682                Sender::ContractInfo(_) => {
683                    panic!("All output-notes must provide a sender-encryption")
684                }
685            });
686
687        pis
688    }
689}
690
691/// The transaction payload
692#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
693#[archive_attr(derive(CheckBytes))]
694pub struct Payload {
695    /// ID of the chain for this transaction to execute on.
696    pub chain_id: u8,
697    /// Transaction skeleton used for the phoenix transaction.
698    pub tx_skeleton: TxSkeleton,
699    /// Data used to calculate the transaction fee and refund unspent gas.
700    pub fee: Fee,
701    /// Data to do a contract call, deployment, or insert a memo.
702    pub data: Option<TransactionData>,
703}
704
705impl PartialEq for Payload {
706    fn eq(&self, other: &Self) -> bool {
707        self.hash() == other.hash()
708    }
709}
710
711impl Eq for Payload {}
712
713impl Payload {
714    fn from_slice_with(
715        buf: &[u8],
716        parse_skeleton: fn(&[u8]) -> Result<TxSkeleton, BytesError>,
717        parse_fee: fn(&mut &[u8]) -> Result<Fee, BytesError>,
718    ) -> Result<Self, BytesError> {
719        let mut buf = buf;
720        let chain_id = u8::from_reader(&mut buf)?;
721        let tx_skeleton = parse_skeleton(read_len_prefixed(&mut buf)?)?;
722        let fee = parse_fee(&mut buf)?;
723        let data = TransactionData::from_slice(buf)?;
724
725        Ok(Self {
726            chain_id,
727            tx_skeleton,
728            fee,
729            data,
730        })
731    }
732
733    /// Serialize the `Payload` into a variable length byte buffer.
734    #[must_use]
735    pub fn to_var_bytes(&self) -> Vec<u8> {
736        let mut bytes = Vec::from([self.chain_id]);
737
738        // serialize the tx-skeleton
739        let skeleton_bytes = self.tx_skeleton.to_var_bytes();
740        bytes.extend((skeleton_bytes.len() as u64).to_bytes());
741        bytes.extend(skeleton_bytes);
742
743        // serialize the fee
744        bytes.extend(self.fee.to_bytes());
745
746        // serialize the transaction data, if present.
747        bytes.extend(TransactionData::option_to_var_bytes(self.data.as_ref()));
748
749        bytes
750    }
751
752    /// Deserialize the Payload from a bytes buffer.
753    ///
754    /// # Errors
755    /// Errors when the bytes are not canonical.
756    pub fn from_slice(buf: &[u8]) -> Result<Self, BytesError> {
757        Self::from_slice_with(buf, TxSkeleton::from_slice, Fee::read_strict)
758    }
759
760    /// Deserialize the payload from bytes using the historical Phoenix
761    /// compatibility parser for finalized ledger data.
762    ///
763    /// # Errors
764    /// Errors when the bytes are not canonical for the historical Phoenix
765    /// ledger-compat parser.
766    pub fn from_slice_ledger_compat(buf: &[u8]) -> Result<Self, BytesError> {
767        Self::from_slice_with(
768            buf,
769            TxSkeleton::from_slice_legacy_compat,
770            Fee::read_legacy_compat,
771        )
772    }
773
774    /// Return input bytes to hash the payload.
775    ///
776    /// Note: The result of this function is *only* meant to be used as an input
777    /// for hashing and *cannot* be used to deserialize the `Payload` again.
778    #[must_use]
779    pub fn to_hash_input_bytes(&self) -> Vec<u8> {
780        let mut bytes = Vec::from([self.chain_id]);
781
782        bytes.extend(self.tx_skeleton.to_hash_input_bytes());
783
784        if let Some(data) = &self.data {
785            bytes.extend(data.signature_message());
786        }
787
788        bytes
789    }
790
791    /// Create the `Payload`-hash to be used as an input to the
792    /// phoenix-transaction circuit.
793    #[must_use]
794    pub fn hash(&self) -> BlsScalar {
795        BlsScalar::hash_to_scalar(&self.to_hash_input_bytes())
796    }
797}
798
799/// The Fee structure
800#[derive(Debug, Clone, Copy, Archive, Serialize, Deserialize)]
801#[archive_attr(derive(CheckBytes))]
802pub struct Fee {
803    /// Gas limit set for a phoenix transaction
804    pub gas_limit: u64,
805    /// Gas price set for a phoenix transaction
806    pub gas_price: u64,
807    /// Address to send the remainder note
808    pub stealth_address: StealthAddress,
809    /// Sender to use for the remainder
810    pub sender: Sender,
811}
812
813impl PartialEq for Fee {
814    fn eq(&self, other: &Self) -> bool {
815        self.sender == other.sender && self.hash() == other.hash()
816    }
817}
818
819impl Eq for Fee {}
820
821impl Fee {
822    const SIZE: usize = 2 * u64::SIZE + StealthAddress::SIZE + Sender::SIZE;
823
824    /// Create a new Fee with inner randomness
825    #[must_use]
826    pub fn new<R: RngCore + CryptoRng>(
827        rng: &mut R,
828        refund_pk: &PublicKey,
829        gas_limit: u64,
830        gas_price: u64,
831    ) -> Self {
832        let r = JubJubScalar::random(&mut *rng);
833
834        let sender_blinder = [
835            JubJubScalar::random(&mut *rng),
836            JubJubScalar::random(&mut *rng),
837        ];
838
839        Self::deterministic(
840            &r,
841            refund_pk,
842            gas_limit,
843            gas_price,
844            &sender_blinder,
845        )
846    }
847
848    /// Create a new Fee without inner randomness
849    #[must_use]
850    pub fn deterministic(
851        r: &JubJubScalar,
852        refund_pk: &PublicKey,
853        gas_limit: u64,
854        gas_price: u64,
855        sender_blinder: &[JubJubScalar; 2],
856    ) -> Self {
857        let refund_address = refund_pk.gen_stealth_address(r);
858        let sender = Sender::encrypt(
859            refund_address.note_pk(),
860            refund_pk,
861            sender_blinder,
862        );
863
864        Fee {
865            gas_limit,
866            gas_price,
867            stealth_address: refund_address,
868            sender,
869        }
870    }
871
872    /// Calculate the max-fee.
873    #[must_use]
874    pub fn max_fee(&self) -> u64 {
875        self.gas_limit * self.gas_price
876    }
877
878    /// Return a hash represented by `H(gas_limit, gas_price, H([note_pk]))`
879    #[must_use]
880    pub fn hash(&self) -> BlsScalar {
881        let npk = self.stealth_address.note_pk().as_ref().to_hash_inputs();
882
883        let hash_inputs = [
884            BlsScalar::from(self.gas_limit),
885            BlsScalar::from(self.gas_price),
886            npk[0],
887            npk[1],
888        ];
889        Hash::digest(Domain::Other, &hash_inputs)[0]
890    }
891
892    /// Generates a remainder from the fee and the given gas consumed.
893    ///
894    /// If there is a deposit, it means that the deposit hasn't been picked up
895    /// by the contract. In this case, it is added to the remainder note.
896    #[must_use]
897    pub fn gen_remainder_note(
898        &self,
899        gas_consumed: u64,
900        deposit: Option<u64>,
901    ) -> Note {
902        // Consuming more gas than the limit provided should never occur, and
903        // it's not the responsibility of the `Fee` to check that.
904        // Here defensively ensure it's not panicking, capping the gas consumed
905        // to the gas limit.
906        let gas_consumed = cmp::min(gas_consumed, self.gas_limit);
907        let gas_changes = (self.gas_limit - gas_consumed) * self.gas_price;
908
909        Note::transparent_stealth(
910            self.stealth_address,
911            gas_changes + deposit.unwrap_or_default(),
912            self.sender,
913        )
914    }
915
916    fn read_strict(reader: &mut &[u8]) -> Result<Self, BytesError> {
917        Self::from_reader(reader)
918    }
919
920    fn read_legacy_compat(reader: &mut &[u8]) -> Result<Self, BytesError> {
921        let bytes = take_fixed_bytes::<{ Fee::SIZE }>(reader)?;
922        let mut reader = &bytes[..];
923        let gas_limit = u64::from_reader(&mut reader)?;
924        let gas_price = u64::from_reader(&mut reader)?;
925
926        let stealth_address =
927            StealthAddress::from_bytes_legacy_compat(take_fixed_bytes::<
928                { StealthAddress::SIZE },
929            >(
930                &mut reader
931            )?)?;
932        let sender = Sender::from_bytes_legacy_compat(take_fixed_bytes::<
933            { Sender::SIZE },
934        >(&mut reader)?)?;
935
936        Ok(Fee {
937            gas_limit,
938            gas_price,
939            stealth_address,
940            sender,
941        })
942    }
943}
944
945const SIZE: usize = Fee::SIZE;
946
947fn read_len_prefixed<'a>(buf: &mut &'a [u8]) -> Result<&'a [u8], BytesError> {
948    let len = usize::try_from(u64::from_reader(buf)?)
949        .map_err(|_| BytesError::InvalidData)?;
950
951    if buf.len() < len {
952        return Err(BytesError::InvalidData);
953    }
954
955    let (bytes, rest) = buf.split_at(len);
956    *buf = rest;
957    Ok(bytes)
958}
959
960fn take_fixed_bytes<'a, const N: usize>(
961    buf: &mut &'a [u8],
962) -> Result<&'a [u8; N], BytesError> {
963    if buf.len() < N {
964        return Err(BytesError::InvalidData);
965    }
966
967    let (bytes, rest) = buf.split_at(N);
968    *buf = rest;
969    bytes.try_into().map_err(|_| BytesError::InvalidData)
970}
971
972impl Serializable<SIZE> for Fee {
973    type Error = BytesError;
974
975    /// Converts a Fee into it's byte representation
976    fn to_bytes(&self) -> [u8; Self::SIZE] {
977        let mut buf = [0u8; Self::SIZE];
978
979        buf[..u64::SIZE].copy_from_slice(&self.gas_limit.to_bytes());
980        let mut start = u64::SIZE;
981        buf[start..start + u64::SIZE]
982            .copy_from_slice(&self.gas_price.to_bytes());
983        start += u64::SIZE;
984        buf[start..start + StealthAddress::SIZE]
985            .copy_from_slice(&self.stealth_address.to_bytes());
986        start += StealthAddress::SIZE;
987        buf[start..start + Sender::SIZE]
988            .copy_from_slice(&self.sender.to_bytes());
989
990        buf
991    }
992
993    /// Attempts to convert a byte representation of a fee into a `Fee`,
994    /// failing if the input is invalid
995    fn from_bytes(bytes: &[u8; Self::SIZE]) -> Result<Self, Self::Error> {
996        let mut reader = &bytes[..];
997
998        let gas_limit = u64::from_reader(&mut reader)?;
999        let gas_price = u64::from_reader(&mut reader)?;
1000        let refund_address = StealthAddress::from_reader(&mut reader)?;
1001        let sender = Sender::from_reader(&mut reader)?;
1002
1003        Ok(Fee {
1004            gas_limit,
1005            gas_price,
1006            stealth_address: refund_address,
1007            sender,
1008        })
1009    }
1010}
1011
1012/// This struct mimics the [`TxCircuit`] but is not generic over the amount of
1013/// input-notes.
1014#[derive(Debug, Clone, PartialEq)]
1015pub struct TxCircuitVec {
1016    /// All information needed in relation to the transaction input-notes
1017    pub input_notes_info: Vec<InputNoteInfo<NOTES_TREE_DEPTH>>,
1018    /// All information needed in relation to the transaction output-notes
1019    pub output_notes_info: [OutputNoteInfo; OUTPUT_NOTES],
1020    /// The hash of the transaction-payload
1021    pub payload_hash: BlsScalar,
1022    /// The root of the tree of notes corresponding to the input-note openings
1023    pub root: BlsScalar,
1024    /// The deposit of the transaction, is zero if there is no deposit
1025    pub deposit: u64,
1026    /// The maximum fee that the transaction may spend
1027    pub max_fee: u64,
1028    /// The public key of the sender used for the sender-encryption
1029    pub sender_pk: PublicKey,
1030    /// The signature of the payload-hash signed with sk.a and sk.b
1031    pub signatures: (SchnorrSignature, SchnorrSignature),
1032}
1033
1034impl TxCircuitVec {
1035    /// Serialize a [`TxCircuitVec`] into a vector of bytes.
1036    #[must_use]
1037    pub fn to_var_bytes(&self) -> Vec<u8> {
1038        let input_len = self.input_notes_info.len();
1039
1040        let mut bytes = Vec::with_capacity(Self::size(input_len));
1041
1042        // first serialize the amount of input-notes
1043        bytes.extend((input_len as u64).to_bytes());
1044
1045        // then serialize the other fields
1046        for info in &self.input_notes_info {
1047            bytes.extend(info.to_var_bytes());
1048        }
1049        for info in &self.output_notes_info {
1050            bytes.extend(info.to_bytes());
1051        }
1052        bytes.extend(self.payload_hash.to_bytes());
1053        bytes.extend(self.root.to_bytes());
1054        bytes.extend(self.deposit.to_bytes());
1055        bytes.extend(self.max_fee.to_bytes());
1056        bytes.extend(self.sender_pk.to_bytes());
1057        bytes.extend(self.signatures.0.to_bytes());
1058        bytes.extend(self.signatures.1.to_bytes());
1059
1060        bytes
1061    }
1062
1063    /// Deserialize a [`TxCircuitVec`] from a slice of bytes.
1064    ///
1065    /// # Errors
1066    ///
1067    /// Will return [`dusk_bytes::Error`] in case of a deserialization error.
1068    pub fn from_slice(bytes: &[u8]) -> Result<Self, BytesError> {
1069        let input_len = u64::from_slice(bytes)?;
1070
1071        // the input-len is smaller than a u32::MAX
1072        #[allow(clippy::cast_possible_truncation)]
1073        if bytes.len() < Self::size(input_len as usize) {
1074            return Err(BytesError::BadLength {
1075                found: bytes.len(),
1076                expected: Self::size(input_len as usize),
1077            });
1078        }
1079
1080        let bytes = &bytes[u64::SIZE..];
1081        let circuit: TxCircuitVec = match input_len {
1082            1 => TxCircuit::<NOTES_TREE_DEPTH, 1>::from_slice(bytes)?.into(),
1083            2 => TxCircuit::<NOTES_TREE_DEPTH, 2>::from_slice(bytes)?.into(),
1084            3 => TxCircuit::<NOTES_TREE_DEPTH, 3>::from_slice(bytes)?.into(),
1085            4 => TxCircuit::<NOTES_TREE_DEPTH, 4>::from_slice(bytes)?.into(),
1086            _ => return Err(BytesError::InvalidData),
1087        };
1088
1089        Ok(circuit)
1090    }
1091
1092    const fn size(input_len: usize) -> usize {
1093        u64::SIZE
1094            + input_len * InputNoteInfo::<NOTES_TREE_DEPTH>::SIZE
1095            + OUTPUT_NOTES * OutputNoteInfo::SIZE
1096            + 2 * BlsScalar::SIZE
1097            + 2 * u64::SIZE
1098            + PublicKey::SIZE
1099            + 2 * SchnorrSignature::SIZE
1100    }
1101}
1102
1103impl<const I: usize> From<TxCircuit<NOTES_TREE_DEPTH, I>> for TxCircuitVec {
1104    fn from(circuit: TxCircuit<NOTES_TREE_DEPTH, I>) -> Self {
1105        TxCircuitVec {
1106            input_notes_info: circuit.input_notes_info.to_vec(),
1107            output_notes_info: circuit.output_notes_info,
1108            payload_hash: circuit.payload_hash,
1109            root: circuit.root,
1110            deposit: circuit.deposit,
1111            max_fee: circuit.max_fee,
1112            sender_pk: circuit.sender_pk,
1113            signatures: circuit.signatures,
1114        }
1115    }
1116}
1117
1118/// This trait can be used to implement different methods to generate a proof
1119/// from the circuit-bytes.
1120pub trait Prove {
1121    /// Generate a transaction proof from all the information needed to create a
1122    /// tx-circuit.
1123    ///
1124    /// # Errors
1125    /// This function errors in case of an incorrect circuit or of an
1126    /// unobtainable prover-key.
1127    //
1128    // Note that the reference to `self` is needed to plug in a running client
1129    // when delegating the proof generation.
1130    fn prove(&self, tx_circuit_vec_bytes: &[u8]) -> Result<Vec<u8>, Error>;
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135    use super::*;
1136
1137    const HISTORICAL_R: [u8; 32] = [
1138        0x93, 0x98, 0xf3, 0x42, 0x62, 0x20, 0x2d, 0x7f, 0x24, 0x57, 0x74, 0xef,
1139        0xd6, 0x51, 0xa5, 0x03, 0xcb, 0x7a, 0x4f, 0x6d, 0x7c, 0xcf, 0x23, 0x40,
1140        0x4d, 0x17, 0xc2, 0xbf, 0xf3, 0x0e, 0x68, 0xa9,
1141    ];
1142
1143    const HISTORICAL_NOTE_PK: [u8; 32] = [
1144        0x0a, 0x8e, 0x2e, 0x57, 0xa2, 0x2d, 0x40, 0xb5, 0x51, 0x22, 0x38, 0x9f,
1145        0x32, 0x41, 0x7f, 0x20, 0xf8, 0x4d, 0xd7, 0x0b, 0x33, 0x88, 0x60, 0x42,
1146        0xe4, 0x44, 0xc4, 0x5f, 0x5d, 0x50, 0x4b, 0x8d,
1147    ];
1148
1149    fn historical_note_bytes() -> [u8; Note::SIZE] {
1150        let mut bytes = [0u8; Note::SIZE];
1151        bytes[0] = 0; // transparent note
1152
1153        let mut offset = 1 + JubJubAffine::SIZE;
1154        bytes[offset..offset + 32].copy_from_slice(&HISTORICAL_R);
1155        bytes[offset + 32..offset + 64].copy_from_slice(&HISTORICAL_NOTE_PK);
1156        offset += StealthAddress::SIZE + u64::SIZE + NOTE_VAL_ENC_SIZE;
1157
1158        bytes[offset] = 1; // Sender::ContractInfo
1159        bytes
1160    }
1161
1162    fn historical_protocol_tx_bytes() -> Vec<u8> {
1163        let mut skeleton = Vec::new();
1164        skeleton.extend(BlsScalar::from(0u64).to_bytes());
1165        skeleton.extend(0u64.to_bytes());
1166        skeleton.extend(historical_note_bytes());
1167        skeleton.extend(Note::empty().to_bytes());
1168        skeleton.extend(0u64.to_bytes());
1169        skeleton.extend(0u64.to_bytes());
1170
1171        let mut fee = [0u8; Fee::SIZE];
1172        fee[2 * u64::SIZE..2 * u64::SIZE + StealthAddress::SIZE]
1173            .copy_from_slice(&StealthAddress::default().to_bytes());
1174        fee[2 * u64::SIZE + StealthAddress::SIZE] = 1;
1175
1176        let mut payload = Vec::new();
1177        payload.push(0);
1178        payload.extend((skeleton.len() as u64).to_bytes());
1179        payload.extend(skeleton);
1180        payload.extend(fee);
1181        payload.extend(TransactionData::option_to_var_bytes(None));
1182
1183        let mut tx = Vec::new();
1184        tx.push(0);
1185        tx.extend((payload.len() as u64).to_bytes());
1186        tx.extend(payload);
1187        tx.extend(0u64.to_bytes());
1188        tx
1189    }
1190
1191    #[test]
1192    fn tx_skeleton_from_slice_rejects_unbounded_nullifier_count_without_panicking()
1193     {
1194        let mut bytes = Vec::new();
1195        bytes.extend(BlsScalar::from(0u64).to_bytes());
1196        bytes.extend(u64::MAX.to_bytes());
1197
1198        // Keep fixed-tail lengths valid while making nullifier count
1199        // intentionally inconsistent with available bytes.
1200        let min_tail_len = OUTPUT_NOTES * Note::SIZE + u64::SIZE + u64::SIZE;
1201        bytes.resize(BlsScalar::SIZE + u64::SIZE + min_tail_len, 0u8);
1202
1203        let err = TxSkeleton::from_slice(&bytes).unwrap_err();
1204        assert_eq!(err, BytesError::InvalidData);
1205    }
1206
1207    #[test]
1208    fn ledger_compat_accepts_historical_non_subgroup_note_pk() {
1209        let tx = historical_protocol_tx_bytes();
1210
1211        assert!(
1212            crate::transfer::Transaction::decode_with_format(
1213                crate::transfer::TransactionFormat::Aegis,
1214                &tx,
1215            )
1216            .is_err()
1217        );
1218        assert!(
1219            crate::transfer::Transaction::decode_with_format(
1220                crate::transfer::TransactionFormat::PreAegis,
1221                &tx,
1222            )
1223            .is_ok()
1224        );
1225    }
1226}