Skip to main content

dusk_node_data/ledger/
transaction.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
7use dusk_bytes::Serializable as DuskSerializable;
8use dusk_core::signatures::bls::PublicKey as AccountPublicKey;
9use dusk_core::transfer::moonlight::Transaction as MoonlightTransaction;
10use dusk_core::transfer::phoenix::Transaction as PhoenixTransaction;
11use dusk_core::transfer::{
12    Transaction as ProtocolTransaction, TransactionFormat,
13};
14use serde::Serialize;
15use sha3::Digest;
16
17use crate::hard_fork;
18
19/// Number of bytes used by the outer ledger transaction header.
20///
21/// The header contains the transaction version, transaction type, and
22/// length-prefix for the encoded protocol transaction.
23pub(crate) const LEDGER_TRANSACTION_HEADER_BYTES: usize =
24    std::mem::size_of::<u32>() * 3;
25
26/// Canonical protocol transaction representation for a specific
27/// [`TransactionFormat`].
28#[derive(Debug, Clone)]
29pub struct CanonicalTransaction {
30    protocol: ProtocolTransaction,
31    format: TransactionFormat,
32}
33
34impl CanonicalTransaction {
35    /// Canonicalizes a native protocol transaction using the ledger format for
36    /// `block_height`.
37    pub fn canonicalize_for_ledger(
38        protocol: ProtocolTransaction,
39        block_height: u64,
40    ) -> Self {
41        Self::canonicalize(
42            protocol,
43            hard_fork::ledger_tx_format_at(block_height),
44        )
45    }
46
47    /// Canonicalizes a native protocol transaction using the ingress format for
48    /// `block_height`.
49    pub fn canonicalize_for_ingress(
50        protocol: ProtocolTransaction,
51        block_height: u64,
52    ) -> Self {
53        Self::canonicalize(
54            protocol,
55            hard_fork::ingress_tx_format_at(block_height),
56        )
57    }
58
59    /// Decodes transaction bytes for live ingress at `block_height`.
60    ///
61    /// Live network ingress accepts the currently supported transport
62    /// envelopes (`Aegis` and `Boreas`) and normalizes them to the canonical
63    /// ingress format for that height. Historical `PreAegis` encodings remain
64    /// replay-only and are rejected here.
65    pub fn decode_for_ingress(
66        bytes: &[u8],
67        block_height: u64,
68    ) -> Result<Self, dusk_bytes::Error> {
69        let decoded = Self::decode_any(bytes)?;
70
71        if decoded.format() == TransactionFormat::PreAegis {
72            return Err(dusk_bytes::Error::InvalidData);
73        }
74
75        Ok(decoded.reformat_for_ingress(block_height))
76    }
77
78    /// Decodes transaction bytes using the ledger format for `block_height`.
79    ///
80    /// This is the format used by block and ledger replay at that height.
81    pub fn decode_for_ledger(
82        bytes: &[u8],
83        block_height: u64,
84    ) -> Result<Self, dusk_bytes::Error> {
85        Self::decode_with_selected_format(
86            bytes,
87            hard_fork::ledger_tx_format_at(block_height),
88        )
89    }
90
91    /// Decodes transaction bytes using the format encoded by the payload.
92    pub fn decode_any(bytes: &[u8]) -> Result<Self, dusk_bytes::Error> {
93        let decoded = ProtocolTransaction::decode_any(bytes)?;
94        Ok(Self::from_parts(decoded.transaction, decoded.format))
95    }
96
97    pub fn canonicalize(
98        protocol: ProtocolTransaction,
99        format: TransactionFormat,
100    ) -> Self {
101        Self::from_parts(protocol, format)
102    }
103
104    fn decode_with_selected_format(
105        bytes: &[u8],
106        format: TransactionFormat,
107    ) -> Result<Self, dusk_bytes::Error> {
108        let decoded = ProtocolTransaction::decode_with_format(format, bytes)?;
109        Ok(Self::from_parts(decoded.transaction, decoded.format))
110    }
111
112    fn from_parts(
113        protocol: ProtocolTransaction,
114        format: TransactionFormat,
115    ) -> Self {
116        Self { protocol, format }
117    }
118
119    fn digest_bytes(
120        protocol: &ProtocolTransaction,
121        format: TransactionFormat,
122    ) -> [u8; 32] {
123        let tx_bytes = protocol.blob_to_memo().map_or_else(
124            || protocol.encode_for_format(format),
125            |mut blob_tx| {
126                let _ = blob_tx.strip_blobs();
127                blob_tx.encode_for_format(format)
128            },
129        );
130
131        sha3::Sha3_256::digest(tx_bytes).into()
132    }
133
134    /// Returns the native protocol transaction.
135    pub fn protocol(&self) -> &ProtocolTransaction {
136        &self.protocol
137    }
138
139    /// Returns the serialization format used for the protocol transaction.
140    pub fn format(&self) -> TransactionFormat {
141        self.format
142    }
143
144    /// Reformats a canonical transaction to the ingress format active at
145    /// `block_height`.
146    pub fn reformat_for_ingress(&self, block_height: u64) -> Self {
147        let expected = hard_fork::ingress_tx_format_at(block_height);
148
149        if self.format() == expected {
150            return self.clone();
151        }
152
153        Self::canonicalize(self.protocol.clone(), expected)
154    }
155
156    /// Encodes the native protocol transaction using its selected format.
157    pub fn protocol_bytes(&self) -> Vec<u8> {
158        self.protocol.encode_for_format(self.format)
159    }
160
161    /// Computes the transaction ID.
162    ///
163    /// This is the protocol-level transaction hash used to identify the
164    /// transaction.
165    pub fn id(&self) -> [u8; 32] {
166        self.protocol.hash().to_bytes()
167    }
168
169    /// Computes the ledger digest of the transaction data.
170    ///
171    /// This digest hashes the encoded transaction data used by the ledger state
172    /// root. For blob transactions, sidecar data is stripped first.
173    pub fn digest(&self) -> [u8; 32] {
174        Self::digest_bytes(&self.protocol, self.format)
175    }
176
177    pub fn gas_price(&self) -> u64 {
178        self.protocol.gas_price()
179    }
180
181    pub fn to_spend_ids(&self) -> Vec<SpendingId> {
182        match &self.protocol {
183            ProtocolTransaction::Phoenix(p) => p
184                .nullifiers()
185                .iter()
186                .map(|n| SpendingId::Nullifier(n.to_bytes()))
187                .collect(),
188            ProtocolTransaction::Moonlight(m) => {
189                vec![SpendingId::AccountNonce(*m.sender(), m.nonce())]
190            }
191        }
192    }
193
194    pub fn next_spending_id(&self) -> Option<SpendingId> {
195        match &self.protocol {
196            ProtocolTransaction::Phoenix(_) => None,
197            ProtocolTransaction::Moonlight(m) => {
198                Some(SpendingId::AccountNonce(*m.sender(), m.nonce() + 1))
199            }
200        }
201    }
202}
203
204/// Ledger transaction wrapper around a [`CanonicalTransaction`].
205#[derive(Debug, Clone)]
206pub struct LedgerTransaction {
207    pub version: u32,
208    pub r#type: u32,
209    canonical: CanonicalTransaction,
210}
211
212impl LedgerTransaction {
213    /// Returns the encoded ledger transaction size, including the outer header.
214    pub fn size(&self) -> usize {
215        LEDGER_TRANSACTION_HEADER_BYTES + self.protocol_bytes().len()
216    }
217}
218
219impl From<CanonicalTransaction> for LedgerTransaction {
220    fn from(value: CanonicalTransaction) -> Self {
221        Self {
222            r#type: 1,
223            version: 1,
224            canonical: value,
225        }
226    }
227}
228
229/// A spent transaction is a transaction that has been included in a block and
230/// was executed.
231#[derive(Debug, Clone, Serialize)]
232pub struct SpentTransaction {
233    /// The transaction that was executed.
234    pub inner: LedgerTransaction,
235    /// The height of the block in which the transaction was included.
236    pub block_height: u64,
237    /// The amount of gas that was spent during the execution of the
238    /// transaction.
239    pub gas_spent: u64,
240    /// An optional error message if the transaction execution yielded an
241    /// error.
242    pub err: Option<String>,
243}
244
245impl SpentTransaction {
246    /// Returns the underlying public transaction, if it is one. Otherwise,
247    /// returns `None`.
248    pub fn public(&self) -> Option<&MoonlightTransaction> {
249        match self.inner.protocol() {
250            ProtocolTransaction::Moonlight(public_tx) => Some(public_tx),
251            _ => None,
252        }
253    }
254
255    /// Returns the underlying shielded transaction, if it is one. Otherwise,
256    /// returns `None`.
257    pub fn shielded(&self) -> Option<&PhoenixTransaction> {
258        match self.inner.protocol() {
259            ProtocolTransaction::Phoenix(shielded_tx) => Some(shielded_tx),
260            _ => None,
261        }
262    }
263}
264
265impl LedgerTransaction {
266    pub fn decode_for_ingress(
267        bytes: &[u8],
268        block_height: u64,
269    ) -> Result<Self, dusk_bytes::Error> {
270        CanonicalTransaction::decode_for_ingress(bytes, block_height)
271            .map(Into::into)
272    }
273
274    pub fn decode_for_ledger(
275        bytes: &[u8],
276        block_height: u64,
277    ) -> Result<Self, dusk_bytes::Error> {
278        CanonicalTransaction::decode_for_ledger(bytes, block_height)
279            .map(Into::into)
280    }
281
282    pub fn decode_any(bytes: &[u8]) -> Result<Self, dusk_bytes::Error> {
283        CanonicalTransaction::decode_any(bytes).map(Into::into)
284    }
285
286    /// Builds a ledger transaction from a native protocol transaction and an
287    /// explicit format.
288    pub fn from_protocol_with_format(
289        protocol: ProtocolTransaction,
290        format: TransactionFormat,
291    ) -> Self {
292        CanonicalTransaction::canonicalize(protocol, format).into()
293    }
294
295    pub fn from_protocol_for_ledger(
296        protocol: ProtocolTransaction,
297        block_height: u64,
298    ) -> Self {
299        CanonicalTransaction::canonicalize_for_ledger(protocol, block_height)
300            .into()
301    }
302
303    pub fn from_protocol_for_ingress(
304        protocol: ProtocolTransaction,
305        block_height: u64,
306    ) -> Self {
307        CanonicalTransaction::canonicalize_for_ingress(protocol, block_height)
308            .into()
309    }
310
311    /// Reformats a ledger transaction to the ledger format active at
312    /// `block_height`.
313    pub fn reformat_for_ledger(&self, block_height: u64) -> Self {
314        let expected = hard_fork::ledger_tx_format_at(block_height);
315
316        if self.format() == expected {
317            return self.clone();
318        }
319
320        Self::from_protocol_with_format(self.protocol().clone(), expected)
321    }
322
323    /// Reformats a ledger transaction to the ingress format active at
324    /// `block_height`.
325    pub fn reformat_for_ingress(&self, block_height: u64) -> Self {
326        let expected = hard_fork::ingress_tx_format_at(block_height);
327
328        if self.format() == expected {
329            return self.clone();
330        }
331
332        Self::from_protocol_with_format(self.protocol().clone(), expected)
333    }
334
335    pub fn format(&self) -> TransactionFormat {
336        self.canonical.format()
337    }
338
339    pub fn canonical(&self) -> &CanonicalTransaction {
340        &self.canonical
341    }
342
343    pub fn protocol(&self) -> &ProtocolTransaction {
344        self.canonical.protocol()
345    }
346
347    pub fn protocol_bytes(&self) -> Vec<u8> {
348        self.canonical.protocol_bytes()
349    }
350
351    /// Computes the hash digest of the entire transaction data.
352    ///
353    /// This method returns the Sha3 256 digest of the entire
354    /// transaction in its serialized form. If the transaction is a blob
355    /// transaction, the sidecar is stripped
356    ///
357    /// The digest hash is currently only being used in the merkle tree.
358    ///
359    /// ### Returns
360    /// An array of 32 bytes representing the hash of the transaction.
361    pub fn digest(&self) -> [u8; 32] {
362        self.canonical.digest()
363    }
364
365    /// Computes the transaction ID.
366    ///
367    /// The transaction ID is a unique identifier for the transaction.
368    /// Unlike the [`digest()`](#method.digest) method, which is computed over
369    /// the entire transaction, the transaction ID is derived from specific
370    /// fields of the transaction and serves as a unique identifier of the
371    /// transaction itself.
372    ///
373    /// ### Returns
374    /// An array of 32 bytes representing the transaction ID.
375    pub fn id(&self) -> [u8; 32] {
376        self.canonical.id()
377    }
378
379    pub fn gas_price(&self) -> u64 {
380        self.protocol().gas_price()
381    }
382
383    pub fn to_spend_ids(&self) -> Vec<SpendingId> {
384        self.canonical.to_spend_ids()
385    }
386
387    pub fn next_spending_id(&self) -> Option<SpendingId> {
388        self.canonical.next_spending_id()
389    }
390
391    pub fn blob_mut(
392        &mut self,
393    ) -> Option<&mut Vec<dusk_core::transfer::data::BlobData>> {
394        self.canonical.protocol.blob_mut()
395    }
396
397    pub fn strip_blobs(
398        &mut self,
399    ) -> Option<Vec<([u8; 32], dusk_core::transfer::data::BlobSidecar)>> {
400        self.canonical.protocol.strip_blobs()
401    }
402}
403
404impl PartialEq<Self> for LedgerTransaction {
405    fn eq(&self, other: &Self) -> bool {
406        self.r#type == other.r#type
407            && self.version == other.version
408            && self.format() == other.format()
409            && self.id() == other.id()
410    }
411}
412
413impl Eq for LedgerTransaction {}
414
415impl PartialEq<Self> for SpentTransaction {
416    fn eq(&self, other: &Self) -> bool {
417        self.inner == other.inner && self.gas_spent == other.gas_spent
418    }
419}
420
421impl Eq for SpentTransaction {}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
424pub enum SpendingId {
425    Nullifier([u8; 32]),
426    AccountNonce(AccountPublicKey, u64),
427}
428
429impl SpendingId {
430    pub fn to_bytes(&self) -> Vec<u8> {
431        match self {
432            SpendingId::Nullifier(n) => n.to_vec(),
433            SpendingId::AccountNonce(account, nonce) => {
434                let mut id = account.to_bytes().to_vec();
435                id.extend_from_slice(&nonce.to_le_bytes());
436                id
437            }
438        }
439    }
440
441    pub fn next(&self) -> Option<SpendingId> {
442        match self {
443            SpendingId::Nullifier(_) => None,
444            SpendingId::AccountNonce(account, nonce) => {
445                Some(SpendingId::AccountNonce(*account, nonce + 1))
446            }
447        }
448    }
449}
450
451#[cfg(any(feature = "faker", test))]
452pub mod faker {
453    use dusk_core::transfer::data::{ContractCall, TransactionData};
454    use dusk_core::transfer::phoenix::{
455        Fee, Note, Payload as PhoenixPayload, PublicKey as PhoenixPublicKey,
456        SecretKey as PhoenixSecretKey, Transaction as PhoenixTransaction,
457        TxSkeleton,
458    };
459    use dusk_core::{BlsScalar, JubJubScalar};
460    use rand::Rng;
461
462    use super::*;
463    use crate::ledger::Dummy;
464
465    impl<T> Dummy<T> for LedgerTransaction {
466        fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
467            gen_dummy_tx(1_000_000)
468        }
469    }
470
471    impl<T> Dummy<T> for SpentTransaction {
472        fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
473            let tx = LedgerTransaction::from_protocol_with_format(
474                gen_dummy_tx(1_000_000).protocol().clone(),
475                TransactionFormat::PreAegis,
476            );
477            SpentTransaction {
478                inner: tx,
479                block_height: 0,
480                gas_spent: 3,
481                err: Some("error".to_string()),
482            }
483        }
484    }
485
486    /// Generates a decodable transaction from a fixed blob with a specified
487    /// gas price.
488    pub fn gen_dummy_tx(gas_price: u64) -> LedgerTransaction {
489        let pk = PhoenixPublicKey::from(&PhoenixSecretKey::new(
490            JubJubScalar::from(42u64),
491            JubJubScalar::from(42u64),
492        ));
493        let gas_limit = 1;
494
495        let fee = Fee::deterministic(
496            &JubJubScalar::from(5u64),
497            &pk,
498            gas_limit,
499            gas_price,
500            &[JubJubScalar::from(9u64), JubJubScalar::from(10u64)],
501        );
502
503        let tx_skeleton = TxSkeleton {
504            root: BlsScalar::from(12345u64),
505            nullifiers: vec![
506                BlsScalar::from(1u64),
507                BlsScalar::from(2u64),
508                BlsScalar::from(3u64),
509            ],
510            outputs: [Note::empty(), Note::empty()],
511            max_fee: gas_price * gas_limit,
512            deposit: 0,
513        };
514
515        let contract_call = ContractCall::new([21; 32], "some_method");
516
517        let payload = PhoenixPayload {
518            chain_id: 0xFA,
519            tx_skeleton,
520            fee,
521            data: Some(TransactionData::Call(contract_call)),
522        };
523        let proof = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
524
525        let tx: ProtocolTransaction =
526            PhoenixTransaction::from_payload_and_proof(payload, proof).into();
527
528        LedgerTransaction::from_protocol_with_format(
529            tx,
530            TransactionFormat::Aegis,
531        )
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use dusk_core::transfer::TransactionFormat;
538
539    use super::faker::gen_dummy_tx;
540    use super::*;
541
542    #[test]
543    fn decode_for_ingress_accepts_aegis_and_normalizes_to_boreas() {
544        let tx = gen_dummy_tx(10);
545        let bytes = tx.protocol_bytes();
546
547        let decoded =
548            CanonicalTransaction::decode_for_ingress(&bytes, u64::MAX)
549                .expect("aegis bytes should decode for boreas ingress");
550
551        assert_eq!(decoded.format(), TransactionFormat::Boreas);
552        assert_eq!(decoded.id(), tx.id());
553    }
554
555    #[test]
556    fn decode_for_ingress_accepts_boreas_and_normalizes_to_aegis() {
557        let tx = gen_dummy_tx(10);
558        let bytes = tx.protocol().encode_for_format(TransactionFormat::Boreas);
559
560        let decoded = CanonicalTransaction::decode_for_ingress(&bytes, 1)
561            .expect("boreas bytes should decode for aegis ingress");
562
563        assert_eq!(decoded.format(), TransactionFormat::Aegis);
564        assert_eq!(decoded.id(), tx.id());
565    }
566
567    #[test]
568    fn reformat_for_ingress_preserves_tx_identity() {
569        let tx = gen_dummy_tx(10);
570        let reformatted = tx.reformat_for_ingress(u64::MAX);
571
572        assert_eq!(reformatted.format(), TransactionFormat::Boreas);
573        assert_eq!(reformatted.id(), tx.id());
574    }
575
576    #[test]
577    fn reformat_for_ingress_preserves_tx_identity_before_boreas() {
578        let tx = LedgerTransaction::from_protocol_with_format(
579            gen_dummy_tx(10).protocol().clone(),
580            TransactionFormat::Boreas,
581        );
582        let reformatted = tx.reformat_for_ingress(1);
583
584        assert_eq!(reformatted.format(), TransactionFormat::Aegis);
585        assert_eq!(reformatted.id(), tx.id());
586    }
587}