dusk_core/transfer/
moonlight.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 moonlight transaction model of Dusk's transfer
8//! contract.
9
10#[cfg(feature = "serde")]
11use serde_with::{serde_as, DisplayFromStr};
12
13use alloc::vec::Vec;
14
15use bytecheck::CheckBytes;
16use dusk_bytes::{DeserializableSlice, Error as BytesError, Serializable};
17use rkyv::{Archive, Deserialize, Serialize};
18
19use crate::signatures::bls::{
20    PublicKey as AccountPublicKey, SecretKey as AccountSecretKey,
21    Signature as AccountSignature,
22};
23use crate::transfer::data::{
24    BlobData, ContractBytecode, ContractCall, ContractDeploy, TransactionData,
25    MAX_MEMO_SIZE,
26};
27use crate::{BlsScalar, Error};
28
29/// A Moonlight account's information.
30#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
31#[archive_attr(derive(CheckBytes))]
32#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct AccountData {
35    /// Number used for replay protection.
36    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
37    pub nonce: u64,
38    /// Account balance.
39    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
40    pub balance: u64,
41}
42
43/// Moonlight transaction.
44#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
45#[archive_attr(derive(CheckBytes))]
46pub struct Transaction {
47    payload: Payload,
48    signature: AccountSignature,
49}
50
51impl Transaction {
52    /// Create a new transaction.
53    ///
54    /// # Errors
55    /// The creation of a transaction is not possible and will error if:
56    /// - the memo, if given, is too large
57    #[allow(clippy::too_many_arguments)]
58    pub fn new(
59        sender_sk: &AccountSecretKey,
60        receiver: Option<AccountPublicKey>,
61        value: u64,
62        deposit: u64,
63        gas_limit: u64,
64        gas_price: u64,
65        nonce: u64,
66        chain_id: u8,
67        data: Option<impl Into<TransactionData>>,
68    ) -> Result<Self, Error> {
69        let refund_address = AccountPublicKey::from(sender_sk);
70
71        Self::new_with_refund(
72            sender_sk,
73            &refund_address,
74            receiver,
75            value,
76            deposit,
77            gas_limit,
78            gas_price,
79            nonce,
80            chain_id,
81            data,
82        )
83    }
84
85    /// Create a new transaction with a specified refund-address for the gas
86    /// refund.
87    ///
88    /// # Errors
89    /// The creation of a transaction is not possible and will error if:
90    /// - the memo, if given, is too large
91    #[allow(clippy::too_many_arguments)]
92    pub fn new_with_refund(
93        sender_sk: &AccountSecretKey,
94        refund_pk: &AccountPublicKey,
95        receiver: Option<AccountPublicKey>,
96        value: u64,
97        deposit: u64,
98        gas_limit: u64,
99        gas_price: u64,
100        nonce: u64,
101        chain_id: u8,
102        data: Option<impl Into<TransactionData>>,
103    ) -> Result<Self, Error> {
104        let data = data.map(Into::into);
105        let sender = AccountPublicKey::from(sender_sk);
106        let receiver = receiver.unwrap_or(sender);
107
108        let fee = Fee {
109            gas_limit,
110            gas_price,
111            refund_address: *refund_pk,
112        };
113
114        let payload = Payload {
115            chain_id,
116            sender,
117            receiver,
118            value,
119            deposit,
120            fee,
121            nonce,
122            data,
123        };
124
125        Self::sign_payload(sender_sk, payload)
126    }
127
128    /// Create a transaction by signing a previously generated payload with a
129    /// given secret-key.
130    ///
131    /// Note that this transaction will be invalid if the secret-key used for
132    /// signing doesn't form a valid key-pair with the public-key of the
133    /// `sender`.
134    ///
135    /// # Errors
136    /// The creation of a transaction is not possible and will error if:
137    /// - the payload memo, if given, is too large
138    pub fn sign_payload(
139        sender_sk: &AccountSecretKey,
140        payload: Payload,
141    ) -> Result<Self, Error> {
142        if let Some(TransactionData::Memo(memo)) = payload.data.as_ref() {
143            if memo.len() > MAX_MEMO_SIZE {
144                return Err(Error::MemoTooLarge(memo.len()));
145            }
146        }
147
148        let digest = payload.signature_message();
149        let signature = sender_sk.sign(&digest);
150
151        Ok(Self { payload, signature })
152    }
153
154    /// The proof of the transaction.
155    #[must_use]
156    pub fn signature(&self) -> &AccountSignature {
157        &self.signature
158    }
159
160    /// Return the sender of the transaction.
161    #[must_use]
162    pub fn sender(&self) -> &AccountPublicKey {
163        &self.payload.sender
164    }
165
166    /// Return the address to send the transaction refund to.
167    #[must_use]
168    pub fn refund_address(&self) -> &AccountPublicKey {
169        &self.payload.fee.refund_address
170    }
171
172    /// Return the receiver of the transaction if it's different from the
173    /// sender. Otherwise, return None.
174    #[must_use]
175    pub fn receiver(&self) -> Option<&AccountPublicKey> {
176        if self.payload.sender == self.payload.receiver {
177            None
178        } else {
179            Some(&self.payload.receiver)
180        }
181    }
182
183    /// Return the value transferred in the transaction.
184    #[must_use]
185    pub fn value(&self) -> u64 {
186        self.payload.value
187    }
188
189    /// Returns the deposit of the transaction.
190    #[must_use]
191    pub fn deposit(&self) -> u64 {
192        self.payload.deposit
193    }
194
195    /// Returns the gas limit of the transaction.
196    #[must_use]
197    pub fn gas_limit(&self) -> u64 {
198        self.payload.fee.gas_limit
199    }
200
201    /// Returns the gas price of the transaction.
202    #[must_use]
203    pub fn gas_price(&self) -> u64 {
204        self.payload.fee.gas_price
205    }
206
207    /// Returns the nonce of the transaction.
208    #[must_use]
209    pub fn nonce(&self) -> u64 {
210        self.payload.nonce
211    }
212
213    /// Returns the chain ID of the transaction.
214    #[must_use]
215    pub fn chain_id(&self) -> u8 {
216        self.payload.chain_id
217    }
218
219    /// Return the contract call data, if there is any.
220    #[must_use]
221    pub fn call(&self) -> Option<&ContractCall> {
222        #[allow(clippy::match_wildcard_for_single_variants)]
223        match self.data()? {
224            TransactionData::Call(ref c) => Some(c),
225            _ => None,
226        }
227    }
228
229    /// Return the contract deploy data, if there is any.
230    #[must_use]
231    pub fn deploy(&self) -> Option<&ContractDeploy> {
232        #[allow(clippy::match_wildcard_for_single_variants)]
233        match self.data()? {
234            TransactionData::Deploy(ref d) => Some(d),
235            _ => None,
236        }
237    }
238
239    /// Returns the memo used with the transaction, if any.
240    #[must_use]
241    pub fn memo(&self) -> Option<&[u8]> {
242        match self.data()? {
243            TransactionData::Memo(memo) => Some(memo),
244            _ => None,
245        }
246    }
247
248    /// Return the blob data, if there is any.
249    #[must_use]
250    pub fn blob(&self) -> Option<&Vec<BlobData>> {
251        #[allow(clippy::match_wildcard_for_single_variants)]
252        match self.data()? {
253            TransactionData::Blob(ref d) => Some(d),
254            _ => None,
255        }
256    }
257
258    /// Return the mutable blob data, if there is any.
259    #[must_use]
260    pub fn blob_mut(&mut self) -> Option<&mut Vec<BlobData>> {
261        #[allow(clippy::match_wildcard_for_single_variants)]
262        match self.data_mut()? {
263            TransactionData::Blob(d) => Some(d),
264            _ => None,
265        }
266    }
267
268    /// Returns the transaction data, if it exists.
269    #[must_use]
270    pub fn data(&self) -> Option<&TransactionData> {
271        self.payload.data.as_ref()
272    }
273
274    /// Returns the transaction data, if it exists.
275    #[must_use]
276    pub(crate) fn data_mut(&mut self) -> Option<&mut TransactionData> {
277        self.payload.data.as_mut()
278    }
279
280    /// Creates a modified clone of this transaction if it contains data for
281    /// deployment, clones all fields except for the bytecode 'bytes' part.
282    /// Returns none if the transaction is not a deployment transaction.
283    #[must_use]
284    pub fn strip_off_bytecode(&self) -> Option<Self> {
285        let deploy = self.deploy()?;
286
287        let stripped_deploy = TransactionData::Deploy(ContractDeploy {
288            owner: deploy.owner.clone(),
289            init_args: deploy.init_args.clone(),
290            bytecode: ContractBytecode {
291                hash: deploy.bytecode.hash,
292                bytes: Vec::new(),
293            },
294            nonce: deploy.nonce,
295        });
296
297        let mut stripped_transaction = self.clone();
298        stripped_transaction.payload.data = Some(stripped_deploy);
299
300        Some(stripped_transaction)
301    }
302
303    /// Creates a modified clone of this transaction if it contains a Blob,
304    /// clones all fields except for the Blob, whose versioned hashes are set as
305    /// Memo.
306    ///
307    /// Returns none if the transaction is not a Blob transaction.
308    #[must_use]
309    pub fn blob_to_memo(&self) -> Option<Self> {
310        let data = self.data()?;
311
312        if let TransactionData::Blob(_) = data {
313            let hash = data.signature_message();
314            let memo = TransactionData::Memo(hash);
315            let mut converted_tx = self.clone();
316            converted_tx.payload.data = Some(memo);
317            Some(converted_tx)
318        } else {
319            None
320        }
321    }
322
323    /// Serialize a transaction into a byte buffer.
324    #[must_use]
325    pub fn to_var_bytes(&self) -> Vec<u8> {
326        let mut bytes = Vec::new();
327
328        let payload_bytes = self.payload.to_var_bytes();
329        bytes.extend((payload_bytes.len() as u64).to_bytes());
330        bytes.extend(payload_bytes);
331
332        bytes.extend(self.signature.to_bytes());
333
334        bytes
335    }
336
337    /// Deserialize the Transaction from a bytes buffer.
338    ///
339    /// # Errors
340    /// Errors when the bytes are not canonical.
341    pub fn from_slice(buf: &[u8]) -> Result<Self, BytesError> {
342        let mut buf = buf;
343
344        let payload_len = usize::try_from(u64::from_reader(&mut buf)?)
345            .map_err(|_| BytesError::InvalidData)?;
346
347        if buf.len() < payload_len {
348            return Err(BytesError::InvalidData);
349        }
350        let (payload_buf, new_buf) = buf.split_at(payload_len);
351
352        let payload = Payload::from_slice(payload_buf)?;
353        buf = new_buf;
354
355        let signature = AccountSignature::from_bytes(
356            buf.try_into().map_err(|_| BytesError::InvalidData)?,
357        )
358        .map_err(|_| BytesError::InvalidData)?;
359
360        Ok(Self { payload, signature })
361    }
362
363    /// Return input bytes to hash the payload.
364    ///
365    /// Note: The result of this function is *only* meant to be used as an input
366    /// for hashing and *cannot* be used to deserialize the transaction again.
367    #[must_use]
368    pub fn to_hash_input_bytes(&self) -> Vec<u8> {
369        let mut bytes = self.payload.signature_message();
370        bytes.extend(self.signature.to_bytes());
371        bytes
372    }
373
374    /// Return the message that is meant to be signed over to make the
375    /// transaction a valid one.
376    #[must_use]
377    pub fn signature_message(&self) -> Vec<u8> {
378        self.payload.signature_message()
379    }
380
381    /// Create the transaction hash.
382    #[must_use]
383    pub fn hash(&self) -> BlsScalar {
384        BlsScalar::hash_to_scalar(&self.to_hash_input_bytes())
385    }
386}
387
388/// The payload for a moonlight transaction.
389#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
390#[archive_attr(derive(CheckBytes))]
391pub struct Payload {
392    /// ID of the chain for this transaction to execute on.
393    pub chain_id: u8,
394    /// Key of the sender of this transaction.
395    pub sender: AccountPublicKey,
396    /// Key of the receiver of the funds.
397    pub receiver: AccountPublicKey,
398    /// Value to be transferred.
399    pub value: u64,
400    /// Deposit for a contract.
401    pub deposit: u64,
402    /// Data used to calculate the transaction fee and refund unspent gas.
403    pub fee: Fee,
404    /// Nonce used for replay protection. Moonlight nonces are strictly
405    /// increasing and incremental, meaning that for a transaction to be
406    /// valid, only the current nonce + 1 can be used.
407    ///
408    /// The current nonce is queryable via the transfer contract.
409    pub nonce: u64,
410    /// Data to do a contract call, deployment, or insert a memo.
411    pub data: Option<TransactionData>,
412}
413
414impl Payload {
415    /// Serialize the payload into a byte buffer.
416    #[must_use]
417    pub fn to_var_bytes(&self) -> Vec<u8> {
418        let mut bytes = Vec::from([self.chain_id]);
419
420        bytes.extend(self.sender.to_bytes());
421        // to save space we only serialize the receiver if it's different from
422        // the sender
423        if self.sender == self.receiver {
424            bytes.push(0);
425        } else {
426            bytes.push(1);
427            bytes.extend(self.receiver.to_bytes());
428        }
429
430        bytes.extend(self.value.to_bytes());
431        bytes.extend(self.deposit.to_bytes());
432
433        // serialize the fee
434        bytes.extend(self.fee.gas_limit.to_bytes());
435        bytes.extend(self.fee.gas_price.to_bytes());
436        // to save space we only serialize the refund-address if it's different
437        // from the sender
438        if self.sender == self.fee.refund_address {
439            bytes.push(0);
440        } else {
441            bytes.push(1);
442            bytes.extend(self.fee.refund_address.to_bytes());
443        }
444
445        bytes.extend(self.nonce.to_bytes());
446
447        // serialize the transaction data, if present.
448        bytes.extend(TransactionData::option_to_var_bytes(self.data.as_ref()));
449
450        bytes
451    }
452
453    /// Deserialize the payload from bytes slice.
454    ///
455    /// # Errors
456    /// Errors when the bytes are not canonical.
457    pub fn from_slice(buf: &[u8]) -> Result<Self, BytesError> {
458        let mut buf = buf;
459
460        let chain_id = u8::from_reader(&mut buf)?;
461        let sender = AccountPublicKey::from_reader(&mut buf)?;
462        let receiver = match u8::from_reader(&mut buf)? {
463            0 => sender,
464            1 => AccountPublicKey::from_reader(&mut buf)?,
465            _ => {
466                return Err(BytesError::InvalidData);
467            }
468        };
469        let value = u64::from_reader(&mut buf)?;
470        let deposit = u64::from_reader(&mut buf)?;
471
472        // deserialize the fee
473        let gas_limit = u64::from_reader(&mut buf)?;
474        let gas_price = u64::from_reader(&mut buf)?;
475        let refund_address = match u8::from_reader(&mut buf)? {
476            0 => sender,
477            1 => AccountPublicKey::from_reader(&mut buf)?,
478            _ => {
479                return Err(BytesError::InvalidData);
480            }
481        };
482        let fee = Fee {
483            gas_limit,
484            gas_price,
485            refund_address,
486        };
487
488        let nonce = u64::from_reader(&mut buf)?;
489
490        // deserialize optional transaction data
491        let data = TransactionData::from_slice(buf)?;
492
493        Ok(Self {
494            chain_id,
495            sender,
496            receiver,
497            value,
498            deposit,
499            fee,
500            nonce,
501            data,
502        })
503    }
504
505    /// Return input bytes to hash the payload.
506    ///
507    /// Note: The result of this function is *only* meant to be used as an input
508    /// for hashing and *cannot* be used to deserialize the payload again.
509    #[must_use]
510    pub fn signature_message(&self) -> Vec<u8> {
511        let mut bytes = Vec::from([self.chain_id]);
512
513        bytes.extend(self.sender.to_bytes());
514        if self.receiver != self.sender {
515            bytes.extend(self.receiver.to_bytes());
516        }
517        bytes.extend(self.value.to_bytes());
518        bytes.extend(self.deposit.to_bytes());
519        bytes.extend(self.fee.gas_limit.to_bytes());
520        bytes.extend(self.fee.gas_price.to_bytes());
521        if self.fee.refund_address != self.sender {
522            bytes.extend(self.fee.refund_address.to_bytes());
523        }
524        bytes.extend(self.nonce.to_bytes());
525
526        if let Some(data) = &self.data {
527            bytes.extend(data.signature_message());
528        }
529
530        bytes
531    }
532}
533
534/// The Fee structure
535#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
536#[archive_attr(derive(CheckBytes))]
537pub struct Fee {
538    /// Limit on the gas to be spent.
539    pub gas_limit: u64,
540    /// Price for each unit of gas.
541    pub gas_price: u64,
542    /// Address to which to refund the unspent gas.
543    pub refund_address: AccountPublicKey,
544}