Skip to main content

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