monero_serai/
transaction.rs

1use core::cmp::Ordering;
2use std_shims::{
3  vec,
4  vec::Vec,
5  io::{self, Read, Write},
6};
7
8use zeroize::Zeroize;
9
10use curve25519_dalek::edwards::CompressedEdwardsY;
11
12use crate::{
13  io::*,
14  primitives::keccak256,
15  ring_signatures::RingSignature,
16  ringct::{bulletproofs::Bulletproof, PrunedRctProofs},
17};
18
19/// An input in the Monero protocol.
20#[derive(Clone, PartialEq, Eq, Debug)]
21pub enum Input {
22  /// An input for a miner transaction, which is generating new coins.
23  Gen(usize),
24  /// An input spending an output on-chain.
25  ToKey {
26    /// The pool this input spends an output of.
27    amount: Option<u64>,
28    /// The decoys used by this input's ring, specified as their offset distance from each other.
29    key_offsets: Vec<u64>,
30    /// The key image (linking tag, nullifer) for the spent output.
31    key_image: CompressedEdwardsY,
32  },
33}
34
35impl Input {
36  /// Write the Input.
37  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
38    match self {
39      Input::Gen(height) => {
40        w.write_all(&[255])?;
41        write_varint(height, w)
42      }
43
44      Input::ToKey { amount, key_offsets, key_image } => {
45        w.write_all(&[2])?;
46        write_varint(&amount.unwrap_or(0), w)?;
47        write_vec(write_varint, key_offsets, w)?;
48        w.write_all(&key_image.0)
49      }
50    }
51  }
52
53  /// Serialize the Input to a `Vec<u8>`.
54  pub fn serialize(&self) -> Vec<u8> {
55    let mut res = vec![];
56    self.write(&mut res).unwrap();
57    res
58  }
59
60  /// Read an Input.
61  pub fn read<R: Read>(r: &mut R) -> io::Result<Input> {
62    Ok(match read_byte(r)? {
63      255 => Input::Gen(read_varint(r)?),
64      2 => {
65        let amount = read_varint(r)?;
66        // https://github.com/monero-project/monero/
67        //   blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/
68        //   src/cryptonote_basic/cryptonote_format_utils.cpp#L860-L863
69        // A non-RCT 0-amount input can't exist because only RCT TXs can have a 0-amount output
70        // That's why collapsing to None if the amount is 0 is safe, even without knowing if RCT
71        let amount = if amount == 0 { None } else { Some(amount) };
72        Input::ToKey {
73          amount,
74          key_offsets: read_vec(read_varint, r)?,
75          key_image: CompressedEdwardsY(read_bytes(r)?),
76        }
77      }
78      _ => Err(io::Error::other("Tried to deserialize unknown/unused input type"))?,
79    })
80  }
81}
82
83/// An output in the Monero protocol.
84#[derive(Clone, PartialEq, Eq, Debug)]
85pub struct Output {
86  /// The pool this output should be sorted into.
87  pub amount: Option<u64>,
88  /// The key which can spend this output.
89  pub key: CompressedEdwardsY,
90  /// The view tag for this output, as used to accelerate scanning.
91  pub view_tag: Option<u8>,
92}
93
94impl Output {
95  /// Write the Output.
96  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
97    write_varint(&self.amount.unwrap_or(0), w)?;
98    w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
99    w.write_all(&self.key.to_bytes())?;
100    if let Some(view_tag) = self.view_tag {
101      w.write_all(&[view_tag])?;
102    }
103    Ok(())
104  }
105
106  /// Write the Output to a `Vec<u8>`.
107  pub fn serialize(&self) -> Vec<u8> {
108    let mut res = Vec::with_capacity(8 + 1 + 32);
109    self.write(&mut res).unwrap();
110    res
111  }
112
113  /// Read an Output.
114  pub fn read<R: Read>(rct: bool, r: &mut R) -> io::Result<Output> {
115    let amount = read_varint(r)?;
116    let amount = if rct {
117      if amount != 0 {
118        Err(io::Error::other("RCT TX output wasn't 0"))?;
119      }
120      None
121    } else {
122      Some(amount)
123    };
124
125    let view_tag = match read_byte(r)? {
126      2 => false,
127      3 => true,
128      _ => Err(io::Error::other("Tried to deserialize unknown/unused output type"))?,
129    };
130
131    Ok(Output {
132      amount,
133      key: CompressedEdwardsY(read_bytes(r)?),
134      view_tag: if view_tag { Some(read_byte(r)?) } else { None },
135    })
136  }
137}
138
139/// An additional timelock for a Monero transaction.
140///
141/// Monero outputs are locked by a default timelock. If a timelock is explicitly specified, the
142/// longer of the two will be the timelock used.
143#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
144pub enum Timelock {
145  /// No additional timelock.
146  None,
147  /// Additionally locked until this block.
148  Block(usize),
149  /// Additionally locked until this many seconds since the epoch.
150  Time(u64),
151}
152
153impl Timelock {
154  /// Write the Timelock.
155  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
156    match self {
157      Timelock::None => write_varint(&0u8, w),
158      Timelock::Block(block) => write_varint(block, w),
159      Timelock::Time(time) => write_varint(time, w),
160    }
161  }
162
163  /// Serialize the Timelock to a `Vec<u8>`.
164  pub fn serialize(&self) -> Vec<u8> {
165    let mut res = Vec::with_capacity(1);
166    self.write(&mut res).unwrap();
167    res
168  }
169
170  /// Read a Timelock.
171  pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
172    const TIMELOCK_BLOCK_THRESHOLD: usize = 500_000_000;
173
174    let raw = read_varint::<_, u64>(r)?;
175    Ok(if raw == 0 {
176      Timelock::None
177    } else if raw <
178      u64::try_from(TIMELOCK_BLOCK_THRESHOLD)
179        .expect("TIMELOCK_BLOCK_THRESHOLD didn't fit in a u64")
180    {
181      Timelock::Block(usize::try_from(raw).expect(
182        "timelock overflowed usize despite being less than a const representable with a usize",
183      ))
184    } else {
185      Timelock::Time(raw)
186    })
187  }
188}
189
190impl PartialOrd for Timelock {
191  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
192    match (self, other) {
193      (Timelock::None, Timelock::None) => Some(Ordering::Equal),
194      (Timelock::None, _) => Some(Ordering::Less),
195      (_, Timelock::None) => Some(Ordering::Greater),
196      (Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b),
197      (Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b),
198      _ => None,
199    }
200  }
201}
202
203/// The transaction prefix.
204///
205/// This is common to all transaction versions and contains most parts of the transaction needed to
206/// handle it. It excludes any proofs.
207#[derive(Clone, PartialEq, Eq, Debug)]
208pub struct TransactionPrefix {
209  /// The timelock this transaction is additionally constrained by.
210  ///
211  /// All transactions on the blockchain are subject to a 10-block lock. This adds a further
212  /// constraint.
213  pub additional_timelock: Timelock,
214  /// The inputs for this transaction.
215  pub inputs: Vec<Input>,
216  /// The outputs for this transaction.
217  pub outputs: Vec<Output>,
218  /// The additional data included within the transaction.
219  ///
220  /// This is an arbitrary data field, yet is used by wallets for containing the data necessary to
221  /// scan the transaction.
222  pub extra: Vec<u8>,
223}
224
225impl TransactionPrefix {
226  /// Write a TransactionPrefix.
227  ///
228  /// This is distinct from Monero in that it won't write any version.
229  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
230    self.additional_timelock.write(w)?;
231    write_vec(Input::write, &self.inputs, w)?;
232    write_vec(Output::write, &self.outputs, w)?;
233    write_varint(&self.extra.len(), w)?;
234    w.write_all(&self.extra)
235  }
236
237  /// Read a TransactionPrefix.
238  ///
239  /// This is distinct from Monero in that it won't read the version. The version must be passed
240  /// in.
241  pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> {
242    let additional_timelock = Timelock::read(r)?;
243
244    let inputs = read_vec(|r| Input::read(r), r)?;
245    if inputs.is_empty() {
246      Err(io::Error::other("transaction had no inputs"))?;
247    }
248    let is_miner_tx = matches!(inputs[0], Input::Gen { .. });
249
250    let mut prefix = TransactionPrefix {
251      additional_timelock,
252      inputs,
253      outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), r)?,
254      extra: vec![],
255    };
256    prefix.extra = read_vec(read_byte, r)?;
257    Ok(prefix)
258  }
259
260  fn hash(&self, version: u64) -> [u8; 32] {
261    let mut buf = vec![];
262    write_varint(&version, &mut buf).unwrap();
263    self.write(&mut buf).unwrap();
264    keccak256(buf)
265  }
266}
267
268mod sealed {
269  use core::fmt::Debug;
270  use crate::ringct::*;
271  use super::*;
272
273  pub(crate) trait RingSignatures: Clone + PartialEq + Eq + Default + Debug {
274    fn signatures_to_write(&self) -> &[RingSignature];
275    fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self>;
276  }
277
278  impl RingSignatures for Vec<RingSignature> {
279    fn signatures_to_write(&self) -> &[RingSignature] {
280      self
281    }
282    fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self> {
283      let mut signatures = Vec::with_capacity(inputs.len());
284      for input in inputs {
285        match input {
286          Input::ToKey { key_offsets, .. } => {
287            signatures.push(RingSignature::read(key_offsets.len(), r)?)
288          }
289          _ => Err(io::Error::other("reading signatures for a transaction with non-ToKey inputs"))?,
290        }
291      }
292      Ok(signatures)
293    }
294  }
295
296  impl RingSignatures for () {
297    fn signatures_to_write(&self) -> &[RingSignature] {
298      &[]
299    }
300    fn read_signatures(_: &[Input], _: &mut impl Read) -> io::Result<Self> {
301      Ok(())
302    }
303  }
304
305  pub(crate) trait RctProofsTrait: Clone + PartialEq + Eq + Debug {
306    fn write(&self, w: &mut impl Write) -> io::Result<()>;
307    fn read(
308      ring_length: usize,
309      inputs: usize,
310      outputs: usize,
311      r: &mut impl Read,
312    ) -> io::Result<Option<Self>>;
313    fn rct_type(&self) -> RctType;
314    fn base(&self) -> &RctBase;
315  }
316
317  impl RctProofsTrait for RctProofs {
318    fn write(&self, w: &mut impl Write) -> io::Result<()> {
319      self.write(w)
320    }
321    fn read(
322      ring_length: usize,
323      inputs: usize,
324      outputs: usize,
325      r: &mut impl Read,
326    ) -> io::Result<Option<Self>> {
327      RctProofs::read(ring_length, inputs, outputs, r)
328    }
329    fn rct_type(&self) -> RctType {
330      self.rct_type()
331    }
332    fn base(&self) -> &RctBase {
333      &self.base
334    }
335  }
336
337  impl RctProofsTrait for PrunedRctProofs {
338    fn write(&self, w: &mut impl Write) -> io::Result<()> {
339      self.base.write(w, self.rct_type)
340    }
341    fn read(
342      _ring_length: usize,
343      inputs: usize,
344      outputs: usize,
345      r: &mut impl Read,
346    ) -> io::Result<Option<Self>> {
347      Ok(RctBase::read(inputs, outputs, r)?.map(|(rct_type, base)| Self { rct_type, base }))
348    }
349    fn rct_type(&self) -> RctType {
350      self.rct_type
351    }
352    fn base(&self) -> &RctBase {
353      &self.base
354    }
355  }
356
357  pub(crate) trait PotentiallyPruned {
358    type RingSignatures: RingSignatures;
359    type RctProofs: RctProofsTrait;
360  }
361  /// A transaction which isn't pruned.
362  #[derive(Clone, PartialEq, Eq, Debug)]
363  pub struct NotPruned;
364  impl PotentiallyPruned for NotPruned {
365    type RingSignatures = Vec<RingSignature>;
366    type RctProofs = RctProofs;
367  }
368  /// A transaction which is pruned.
369  #[derive(Clone, PartialEq, Eq, Debug)]
370  pub struct Pruned;
371  impl PotentiallyPruned for Pruned {
372    type RingSignatures = ();
373    type RctProofs = PrunedRctProofs;
374  }
375}
376pub use sealed::*;
377
378/// A Monero transaction.
379#[allow(private_bounds, private_interfaces, clippy::large_enum_variant)]
380#[derive(Clone, PartialEq, Eq, Debug)]
381pub enum Transaction<P: PotentiallyPruned = NotPruned> {
382  /// A version 1 transaction, used by the original Cryptonote codebase.
383  V1 {
384    /// The transaction's prefix.
385    prefix: TransactionPrefix,
386    /// The transaction's ring signatures.
387    signatures: P::RingSignatures,
388  },
389  /// A version 2 transaction, used by the RingCT protocol.
390  V2 {
391    /// The transaction's prefix.
392    prefix: TransactionPrefix,
393    /// The transaction's proofs.
394    proofs: Option<P::RctProofs>,
395  },
396}
397
398enum PrunableHash<'a> {
399  V1(&'a [RingSignature]),
400  V2([u8; 32]),
401}
402
403#[allow(private_bounds)]
404impl<P: PotentiallyPruned> Transaction<P> {
405  /// Get the version of this transaction.
406  pub fn version(&self) -> u8 {
407    match self {
408      Transaction::V1 { .. } => 1,
409      Transaction::V2 { .. } => 2,
410    }
411  }
412
413  /// Get the TransactionPrefix of this transaction.
414  pub fn prefix(&self) -> &TransactionPrefix {
415    match self {
416      Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
417    }
418  }
419
420  /// Get a mutable reference to the TransactionPrefix of this transaction.
421  pub fn prefix_mut(&mut self) -> &mut TransactionPrefix {
422    match self {
423      Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
424    }
425  }
426
427  /// Write the Transaction.
428  ///
429  /// Some writable transactions may not be readable if they're malformed, per Monero's consensus
430  /// rules.
431  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
432    write_varint(&self.version(), w)?;
433    match self {
434      Transaction::V1 { prefix, signatures } => {
435        prefix.write(w)?;
436        for ring_sig in signatures.signatures_to_write() {
437          ring_sig.write(w)?;
438        }
439      }
440      Transaction::V2 { prefix, proofs } => {
441        prefix.write(w)?;
442        match proofs {
443          None => w.write_all(&[0])?,
444          Some(proofs) => proofs.write(w)?,
445        }
446      }
447    }
448    Ok(())
449  }
450
451  /// Write the Transaction to a `Vec<u8>`.
452  pub fn serialize(&self) -> Vec<u8> {
453    let mut res = Vec::with_capacity(2048);
454    self.write(&mut res).unwrap();
455    res
456  }
457
458  /// Read a Transaction.
459  pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
460    let version = read_varint(r)?;
461    let prefix = TransactionPrefix::read(r, version)?;
462
463    if version == 1 {
464      let signatures = if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
465        Default::default()
466      } else {
467        P::RingSignatures::read_signatures(&prefix.inputs, r)?
468      };
469
470      Ok(Transaction::V1 { prefix, signatures })
471    } else if version == 2 {
472      let proofs = P::RctProofs::read(
473        prefix.inputs.first().map_or(0, |input| match input {
474          Input::Gen(_) => 0,
475          Input::ToKey { key_offsets, .. } => key_offsets.len(),
476        }),
477        prefix.inputs.len(),
478        prefix.outputs.len(),
479        r,
480      )?;
481
482      Ok(Transaction::V2 { prefix, proofs })
483    } else {
484      Err(io::Error::other("tried to deserialize unknown version"))
485    }
486  }
487
488  // The hash of the transaction.
489  #[allow(clippy::needless_pass_by_value)]
490  fn hash_with_prunable_hash(&self, prunable: PrunableHash<'_>) -> [u8; 32] {
491    match self {
492      Transaction::V1 { prefix, .. } => {
493        let mut buf = Vec::with_capacity(512);
494
495        // We don't use `self.write` as that may write the signatures (if this isn't pruned)
496        write_varint(&self.version(), &mut buf).unwrap();
497        prefix.write(&mut buf).unwrap();
498
499        // We explicitly write the signatures ourselves here
500        let PrunableHash::V1(signatures) = prunable else {
501          panic!("hashing v1 TX with non-v1 prunable data")
502        };
503        for signature in signatures {
504          signature.write(&mut buf).unwrap();
505        }
506
507        keccak256(buf)
508      }
509      Transaction::V2 { prefix, proofs } => {
510        let mut hashes = Vec::with_capacity(96);
511
512        hashes.extend(prefix.hash(2));
513
514        if let Some(proofs) = proofs {
515          let mut buf = Vec::with_capacity(512);
516          proofs.base().write(&mut buf, proofs.rct_type()).unwrap();
517          hashes.extend(keccak256(&buf));
518        } else {
519          // Serialization of RctBase::Null
520          hashes.extend(keccak256([0]));
521        }
522        let PrunableHash::V2(prunable_hash) = prunable else {
523          panic!("hashing v2 TX with non-v2 prunable data")
524        };
525        hashes.extend(prunable_hash);
526
527        keccak256(hashes)
528      }
529    }
530  }
531}
532
533impl Transaction<NotPruned> {
534  /// The hash of the transaction.
535  pub fn hash(&self) -> [u8; 32] {
536    match self {
537      Transaction::V1 { signatures, .. } => {
538        self.hash_with_prunable_hash(PrunableHash::V1(signatures))
539      }
540      Transaction::V2 { proofs, .. } => {
541        self.hash_with_prunable_hash(PrunableHash::V2(if let Some(proofs) = proofs {
542          let mut buf = Vec::with_capacity(1024);
543          proofs.prunable.write(&mut buf, proofs.rct_type()).unwrap();
544          keccak256(buf)
545        } else {
546          [0; 32]
547        }))
548      }
549    }
550  }
551
552  /// Calculate the hash of this transaction as needed for signing it.
553  ///
554  /// This returns None if the transaction is without signatures.
555  pub fn signature_hash(&self) -> Option<[u8; 32]> {
556    Some(match self {
557      Transaction::V1 { prefix, .. } => {
558        if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
559          None?;
560        }
561        self.hash_with_prunable_hash(PrunableHash::V1(&[]))
562      }
563      Transaction::V2 { proofs, .. } => self.hash_with_prunable_hash({
564        let Some(proofs) = proofs else { None? };
565        let mut buf = Vec::with_capacity(1024);
566        proofs.prunable.signature_write(&mut buf).unwrap();
567        PrunableHash::V2(keccak256(buf))
568      }),
569    })
570  }
571
572  fn is_rct_bulletproof(&self) -> bool {
573    match self {
574      Transaction::V1 { .. } => false,
575      Transaction::V2 { proofs, .. } => {
576        let Some(proofs) = proofs else { return false };
577        proofs.rct_type().bulletproof()
578      }
579    }
580  }
581
582  fn is_rct_bulletproof_plus(&self) -> bool {
583    match self {
584      Transaction::V1 { .. } => false,
585      Transaction::V2 { proofs, .. } => {
586        let Some(proofs) = proofs else { return false };
587        proofs.rct_type().bulletproof_plus()
588      }
589    }
590  }
591
592  /// Calculate the transaction's weight.
593  pub fn weight(&self) -> usize {
594    let blob_size = self.serialize().len();
595
596    let bp = self.is_rct_bulletproof();
597    let bp_plus = self.is_rct_bulletproof_plus();
598    if !(bp || bp_plus) {
599      blob_size
600    } else {
601      blob_size +
602        Bulletproof::calculate_bp_clawback(
603          bp_plus,
604          match self {
605            Transaction::V1 { .. } => panic!("v1 transaction was BP(+)"),
606            Transaction::V2 { prefix, .. } => prefix.outputs.len(),
607          },
608        )
609        .0
610    }
611  }
612}
613
614impl From<Transaction<NotPruned>> for Transaction<Pruned> {
615  fn from(tx: Transaction<NotPruned>) -> Transaction<Pruned> {
616    match tx {
617      Transaction::V1 { prefix, .. } => Transaction::V1 { prefix, signatures: () },
618      Transaction::V2 { prefix, proofs } => Transaction::V2 {
619        prefix,
620        proofs: proofs
621          .map(|proofs| PrunedRctProofs { rct_type: proofs.rct_type(), base: proofs.base }),
622      },
623    }
624  }
625}